feat(core): unify plugin config cleaning and enforce reserved fields for tracker and scm

This commit is contained in:
harshitsinghbhandari 2026-04-02 23:53:05 +05:30 committed by Gaurav Bhola
parent 4645f6574b
commit 3e6d2f97be
1 changed files with 54 additions and 32 deletions

View File

@ -61,51 +61,38 @@ const BUILTIN_PLUGINS: Array<{ slot: PluginSlot; name: string; pkg: string }> =
{ slot: "terminal", name: "web", pkg: "@composio/ao-plugin-terminal-web" },
];
/** Extract plugin-specific config from orchestrator config */
function extractPluginConfig(
slot: PluginSlot,
name: string,
config: OrchestratorConfig,
): Record<string, unknown> | undefined {
// Notifiers are configured under config.notifiers.<id>.
// Match by key (e.g. "openclaw") or explicit plugin field.
// 1. Handle Notifier Slot
if (slot === "notifier") {
for (const [notifierName, notifierConfig] of Object.entries(config.notifiers ?? {})) {
for (const [notifierId, notifierConfig] of Object.entries(config.notifiers ?? {})) {
if (!notifierConfig || typeof notifierConfig !== "object") continue;
const configuredPlugin = (notifierConfig as Record<string, unknown>)["plugin"];
const hasExplicitPlugin = typeof configuredPlugin === "string" && configuredPlugin.length > 0;
const matches = hasExplicitPlugin ? configuredPlugin === name : notifierName === name;
const matches = hasExplicitPlugin ? configuredPlugin === name : notifierId === name;
if (matches) {
const rawConfig = notifierConfig as Record<string, unknown>;
return prepareConfig(slot, name, notifierId, notifierConfig, config.configPath);
}
}
}
// Explicitly check for reserved fields to prevent silent stripping/collision.
// 'path' is reserved for local resolution; 'package' is reserved for npm resolution.
if ("package" in rawConfig && "path" in rawConfig) {
throw new Error(
`In notifier "${notifierName}": both "package" and "path" are specified. ` +
`Use "package" for npm plugins or "path" for local plugins, not both.`
);
}
// 2. Handle Tracker and SCM Slots (Project-level)
if (slot === "tracker" || slot === "scm") {
for (const [projectId, project] of Object.entries(config.projects)) {
const entry = slot === "tracker" ? project.tracker : project.scm;
if (!entry || typeof entry !== "object") continue;
// If loading via built-in name or npm package, 'path' is a collision.
// We detect this by checking if 'package' is present OR if no 'path' was
// intended for local resolution (i.e. name refers to a built-in).
const isBuiltin = BUILTIN_PLUGINS.some((b) => b.slot === slot && b.name === name);
if ((rawConfig.package || isBuiltin) && "path" in rawConfig) {
throw new Error(
`In notifier "${notifierName}": "path" is reserved for plugin loading. ` +
`Rename your configuration field to something else (e.g., "apiPath").`
);
}
const configuredPlugin = (entry as Record<string, unknown>)["plugin"];
const hasExplicitPlugin = typeof configuredPlugin === "string" && configuredPlugin.length > 0;
const matches = hasExplicitPlugin ? configuredPlugin === name : false;
// Strip loading metadata fields (plugin, package, path) from config passed to plugin.
const {
plugin: _plugin,
package: _package,
path: _path,
...rest
} = rawConfig;
return config.configPath ? { ...rest, configPath: config.configPath } : rest;
if (matches) {
const sourceId = `projects.${projectId}.${slot}`;
return prepareConfig(slot, name, sourceId, entry as Record<string, unknown>, config.configPath);
}
}
}
@ -113,6 +100,41 @@ function extractPluginConfig(
return undefined;
}
/**
* Internal helper to validate and strip loading metadata from a plugin configuration.
*/
function prepareConfig(
slot: string,
name: string,
sourceId: string,
rawConfig: Record<string, unknown>,
configPath?: string,
): Record<string, unknown> {
// Explicitly check for reserved fields to prevent silent stripping/collision.
// 'path' is reserved for local resolution; 'package' is reserved for npm resolution.
if ("package" in rawConfig && "path" in rawConfig) {
throw new Error(
`In ${slot} "${sourceId}": both "package" and "path" are specified. ` +
`Use "package" for npm plugins or "path" for local plugins, not both.`,
);
}
// If loading via built-in name or npm package, 'path' is a collision.
// We detect this by checking if 'package' is present OR if no 'path' was
// intended for local resolution (i.e. name refers to a built-in).
const isBuiltin = BUILTIN_PLUGINS.some((b) => b.slot === slot && b.name === name);
if ((rawConfig.package || isBuiltin) && "path" in rawConfig) {
throw new Error(
`In ${slot} "${sourceId}": "path" is reserved for plugin loading. ` +
`Rename your configuration field to something else (e.g., "apiPath").`,
);
}
// Strip loading metadata fields (plugin, package, path) from config passed to plugin.
const { plugin: _plugin, package: _package, path: _path, ...rest } = rawConfig;
return configPath ? { ...rest, configPath } : rest;
}
/**
* Find ALL external plugin entries that match a given plugin config.
* Used for manifest.name validation when loading inline tracker/scm/notifier plugins.