mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 07:40:44 +00:00
feat(plugins): report setup descriptor drift (#71194)
This commit is contained in:
@@ -8,6 +8,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
- Gradium: add a bundled text-to-speech provider with voice-note and telephony output support. (#64958) Thanks @LaurentMazare.
|
||||
- Plugins/setup: honor explicit `setup.requiresRuntime: false` as a descriptor-only setup contract while keeping omitted values on the legacy setup-api fallback path. Thanks @vincentkoc.
|
||||
- Plugins/setup: report descriptor/runtime drift when setup-api registrations disagree with `setup.providers` or `setup.cliBackends`, without rejecting legacy setup plugins. Thanks @vincentkoc.
|
||||
- TUI/dependencies: remove direct `cli-highlight` usage from the OpenClaw TUI code-block renderer, keeping themed code coloring without the extra root dependency. Thanks @vincentkoc.
|
||||
- Diagnostics/OTEL: export run, model-call, and tool-execution diagnostic lifecycle events as OTEL spans without retaining live span state. Thanks @vincentkoc.
|
||||
- Plugins/activation: expose activation plan reasons and a richer plan API so callers can inspect why a plugin was selected while preserving existing id-list activation behavior. (#70943) Thanks @vincentkoc.
|
||||
|
||||
@@ -76,7 +76,9 @@ Setup discovery now prefers descriptor-owned ids such as `setup.providers` and
|
||||
`requiresRuntime` keeps the legacy setup-api fallback for compatibility. If more
|
||||
than one discovered plugin claims the same normalized setup provider or CLI
|
||||
backend id, setup lookup refuses the ambiguous owner instead of relying on
|
||||
discovery order.
|
||||
discovery order. When setup runtime does execute, registry diagnostics report
|
||||
drift between `setup.providers` / `setup.cliBackends` and the providers or CLI
|
||||
backends registered by setup-api without blocking legacy plugins.
|
||||
|
||||
### What the loader caches
|
||||
|
||||
|
||||
@@ -338,6 +338,11 @@ Because setup lookup can execute plugin-owned `setup-api` code, normalized
|
||||
discovered plugins. Ambiguous ownership fails closed instead of picking a
|
||||
winner from discovery order.
|
||||
|
||||
When setup runtime does execute, setup registry diagnostics report descriptor
|
||||
drift if `setup-api` registers a provider or CLI backend that the manifest
|
||||
descriptors do not declare, or if a descriptor has no matching runtime
|
||||
registration. These diagnostics are additive and do not reject legacy plugins.
|
||||
|
||||
### setup.providers reference
|
||||
|
||||
| Field | Required | Type | What it means |
|
||||
|
||||
@@ -378,10 +378,85 @@ describe("setup-registry getJiti", () => {
|
||||
cliBackends: [],
|
||||
configMigrations: [],
|
||||
autoEnableProbes: [],
|
||||
diagnostics: [],
|
||||
});
|
||||
expect(mocks.createJiti).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("reports setup descriptor drift without rejecting runtime registrations", () => {
|
||||
const pluginRoot = makeTempDir();
|
||||
fs.writeFileSync(path.join(pluginRoot, "setup-api.js"), "export default {};\n", "utf-8");
|
||||
mocks.loadPluginManifestRegistry.mockReturnValue({
|
||||
plugins: [
|
||||
{
|
||||
id: "openai",
|
||||
rootDir: pluginRoot,
|
||||
setup: {
|
||||
providers: [{ id: "openai" }],
|
||||
cliBackends: ["codex-cli"],
|
||||
requiresRuntime: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
diagnostics: [],
|
||||
});
|
||||
mocks.createJiti.mockImplementation(() => {
|
||||
return () => ({
|
||||
default: {
|
||||
register(api: {
|
||||
registerProvider: (provider: { id: string; label: string; auth: [] }) => void;
|
||||
registerCliBackend: (backend: { id: string; config: { command: string } }) => void;
|
||||
}) {
|
||||
api.registerProvider({
|
||||
id: "anthropic",
|
||||
label: "Anthropic",
|
||||
auth: [],
|
||||
});
|
||||
api.registerCliBackend({
|
||||
id: "claude-cli",
|
||||
config: { command: "claude" },
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
const registry = resolvePluginSetupRegistry({ env: {} });
|
||||
|
||||
expect(registry.providers.map((entry) => entry.provider.id)).toEqual(["anthropic"]);
|
||||
expect(registry.cliBackends.map((entry) => entry.backend.id)).toEqual(["claude-cli"]);
|
||||
expect(registry.diagnostics).toEqual([
|
||||
expect.objectContaining({
|
||||
pluginId: "openai",
|
||||
code: "setup-descriptor-provider-missing-runtime",
|
||||
declaredId: "openai",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
pluginId: "openai",
|
||||
code: "setup-descriptor-provider-runtime-undeclared",
|
||||
runtimeId: "anthropic",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
pluginId: "openai",
|
||||
code: "setup-descriptor-cli-backend-missing-runtime",
|
||||
declaredId: "codex-cli",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
pluginId: "openai",
|
||||
code: "setup-descriptor-cli-backend-runtime-undeclared",
|
||||
runtimeId: "claude-cli",
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("does not report drift when setup descriptors match runtime registrations", () => {
|
||||
mockOpenAiCliBackendRegistration({
|
||||
requiresRuntime: true,
|
||||
});
|
||||
|
||||
expect(resolvePluginSetupRegistry({ env: {} }).diagnostics).toEqual([]);
|
||||
});
|
||||
|
||||
it("does not load setup-api modules from the current working directory", () => {
|
||||
const pluginRoot = makeTempDir();
|
||||
const workspaceRoot = makeTempDir();
|
||||
|
||||
@@ -46,11 +46,26 @@ type SetupAutoEnableProbeEntry = {
|
||||
probe: PluginSetupAutoEnableProbe;
|
||||
};
|
||||
|
||||
export type PluginSetupRegistryDiagnosticCode =
|
||||
| "setup-descriptor-provider-missing-runtime"
|
||||
| "setup-descriptor-provider-runtime-undeclared"
|
||||
| "setup-descriptor-cli-backend-missing-runtime"
|
||||
| "setup-descriptor-cli-backend-runtime-undeclared";
|
||||
|
||||
export type PluginSetupRegistryDiagnostic = {
|
||||
pluginId: string;
|
||||
code: PluginSetupRegistryDiagnosticCode;
|
||||
declaredId?: string;
|
||||
runtimeId?: string;
|
||||
message: string;
|
||||
};
|
||||
|
||||
type PluginSetupRegistry = {
|
||||
providers: SetupProviderEntry[];
|
||||
cliBackends: SetupCliBackendEntry[];
|
||||
configMigrations: SetupConfigMigrationEntry[];
|
||||
autoEnableProbes: SetupAutoEnableProbeEntry[];
|
||||
diagnostics: PluginSetupRegistryDiagnostic[];
|
||||
};
|
||||
|
||||
type SetupAutoEnableReason = {
|
||||
@@ -372,6 +387,75 @@ function findUniqueSetupManifestOwner(params: {
|
||||
return matches.length === 1 ? matches[0] : undefined;
|
||||
}
|
||||
|
||||
function mapNormalizedIds(ids: readonly string[]): Map<string, string> {
|
||||
const mapped = new Map<string, string>();
|
||||
for (const id of ids) {
|
||||
const normalized = normalizeProviderId(id);
|
||||
if (!normalized || mapped.has(normalized)) {
|
||||
continue;
|
||||
}
|
||||
mapped.set(normalized, id);
|
||||
}
|
||||
return mapped;
|
||||
}
|
||||
|
||||
function pushSetupDescriptorDriftDiagnostics(params: {
|
||||
record: PluginManifestRecord;
|
||||
providers: readonly ProviderPlugin[];
|
||||
cliBackends: readonly CliBackendPlugin[];
|
||||
diagnostics: PluginSetupRegistryDiagnostic[];
|
||||
}): void {
|
||||
const declaredProviderIds = params.record.setup?.providers?.map((entry) => entry.id);
|
||||
if (declaredProviderIds) {
|
||||
for (const declaredId of declaredProviderIds) {
|
||||
if (!params.providers.some((provider) => matchesProvider(provider, declaredId))) {
|
||||
params.diagnostics.push({
|
||||
pluginId: params.record.id,
|
||||
code: "setup-descriptor-provider-missing-runtime",
|
||||
declaredId,
|
||||
message: `setup.providers declares "${declaredId}" but setup runtime did not register a matching provider.`,
|
||||
});
|
||||
}
|
||||
}
|
||||
for (const provider of params.providers) {
|
||||
if (!declaredProviderIds.some((declaredId) => matchesProvider(provider, declaredId))) {
|
||||
params.diagnostics.push({
|
||||
pluginId: params.record.id,
|
||||
code: "setup-descriptor-provider-runtime-undeclared",
|
||||
runtimeId: provider.id,
|
||||
message: `setup runtime registered provider "${provider.id}" but setup.providers does not declare it.`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const declaredCliBackendIds = params.record.setup?.cliBackends;
|
||||
if (declaredCliBackendIds) {
|
||||
const declaredCliBackends = mapNormalizedIds(declaredCliBackendIds);
|
||||
const runtimeCliBackends = mapNormalizedIds(params.cliBackends.map((backend) => backend.id));
|
||||
for (const [normalized, declaredId] of declaredCliBackends) {
|
||||
if (!runtimeCliBackends.has(normalized)) {
|
||||
params.diagnostics.push({
|
||||
pluginId: params.record.id,
|
||||
code: "setup-descriptor-cli-backend-missing-runtime",
|
||||
declaredId,
|
||||
message: `setup.cliBackends declares "${declaredId}" but setup runtime did not register a matching CLI backend.`,
|
||||
});
|
||||
}
|
||||
}
|
||||
for (const [normalized, runtimeId] of runtimeCliBackends) {
|
||||
if (!declaredCliBackends.has(normalized)) {
|
||||
params.diagnostics.push({
|
||||
pluginId: params.record.id,
|
||||
code: "setup-descriptor-cli-backend-runtime-undeclared",
|
||||
runtimeId,
|
||||
message: `setup runtime registered CLI backend "${runtimeId}" but setup.cliBackends does not declare it.`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function resolvePluginSetupRegistry(params?: {
|
||||
workspaceDir?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
@@ -397,6 +481,7 @@ export function resolvePluginSetupRegistry(params?: {
|
||||
cliBackends: [],
|
||||
configMigrations: [],
|
||||
autoEnableProbes: [],
|
||||
diagnostics: [],
|
||||
} satisfies PluginSetupRegistry;
|
||||
setCachedSetupValue(setupRegistryCache, cacheKey, empty);
|
||||
return empty;
|
||||
@@ -406,6 +491,7 @@ export function resolvePluginSetupRegistry(params?: {
|
||||
const cliBackends: SetupCliBackendEntry[] = [];
|
||||
const configMigrations: SetupConfigMigrationEntry[] = [];
|
||||
const autoEnableProbes: SetupAutoEnableProbeEntry[] = [];
|
||||
const diagnostics: PluginSetupRegistryDiagnostic[] = [];
|
||||
const providerKeys = new Set<string>();
|
||||
const cliBackendKeys = new Set<string>();
|
||||
|
||||
@@ -423,6 +509,8 @@ export function resolvePluginSetupRegistry(params?: {
|
||||
continue;
|
||||
}
|
||||
|
||||
const recordProviders: ProviderPlugin[] = [];
|
||||
const recordCliBackends: CliBackendPlugin[] = [];
|
||||
const api = buildSetupPluginApi({
|
||||
record,
|
||||
setupSource: setupRegistration.setupSource,
|
||||
@@ -437,6 +525,7 @@ export function resolvePluginSetupRegistry(params?: {
|
||||
pluginId: record.id,
|
||||
provider,
|
||||
});
|
||||
recordProviders.push(provider);
|
||||
},
|
||||
registerCliBackend(backend) {
|
||||
const key = `${record.id}:${normalizeProviderId(backend.id)}`;
|
||||
@@ -448,6 +537,7 @@ export function resolvePluginSetupRegistry(params?: {
|
||||
pluginId: record.id,
|
||||
backend,
|
||||
});
|
||||
recordCliBackends.push(backend);
|
||||
},
|
||||
registerConfigMigration(migrate) {
|
||||
configMigrations.push({
|
||||
@@ -473,6 +563,12 @@ export function resolvePluginSetupRegistry(params?: {
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
pushSetupDescriptorDriftDiagnostics({
|
||||
record,
|
||||
providers: recordProviders,
|
||||
cliBackends: recordCliBackends,
|
||||
diagnostics,
|
||||
});
|
||||
}
|
||||
|
||||
const registry = {
|
||||
@@ -480,6 +576,7 @@ export function resolvePluginSetupRegistry(params?: {
|
||||
cliBackends,
|
||||
configMigrations,
|
||||
autoEnableProbes,
|
||||
diagnostics,
|
||||
} satisfies PluginSetupRegistry;
|
||||
setCachedSetupValue(setupRegistryCache, cacheKey, registry);
|
||||
return registry;
|
||||
|
||||
Reference in New Issue
Block a user