Skip to content

Commit 6bc0dc8

Browse files
authored
feat(plugins): report setup descriptor drift (#71194)
1 parent 3ffd944 commit 6bc0dc8

5 files changed

Lines changed: 181 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ Docs: https://docs.openclaw.ai
88

99
- Gradium: add a bundled text-to-speech provider with voice-note and telephony output support. (#64958) Thanks @LaurentMazare.
1010
- 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.
11+
- Plugins/setup: report descriptor/runtime drift when setup-api registrations disagree with `setup.providers` or `setup.cliBackends`, without rejecting legacy setup plugins. Thanks @vincentkoc.
1112
- 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.
1213
- Diagnostics/OTEL: export run, model-call, and tool-execution diagnostic lifecycle events as OTEL spans without retaining live span state. Thanks @vincentkoc.
1314
- 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.

docs/plugins/architecture-internals.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,9 @@ Setup discovery now prefers descriptor-owned ids such as `setup.providers` and
7676
`requiresRuntime` keeps the legacy setup-api fallback for compatibility. If more
7777
than one discovered plugin claims the same normalized setup provider or CLI
7878
backend id, setup lookup refuses the ambiguous owner instead of relying on
79-
discovery order.
79+
discovery order. When setup runtime does execute, registry diagnostics report
80+
drift between `setup.providers` / `setup.cliBackends` and the providers or CLI
81+
backends registered by setup-api without blocking legacy plugins.
8082

8183
### What the loader caches
8284

docs/plugins/manifest.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,11 @@ Because setup lookup can execute plugin-owned `setup-api` code, normalized
338338
discovered plugins. Ambiguous ownership fails closed instead of picking a
339339
winner from discovery order.
340340

341+
When setup runtime does execute, setup registry diagnostics report descriptor
342+
drift if `setup-api` registers a provider or CLI backend that the manifest
343+
descriptors do not declare, or if a descriptor has no matching runtime
344+
registration. These diagnostics are additive and do not reject legacy plugins.
345+
341346
### setup.providers reference
342347

343348
| Field | Required | Type | What it means |

src/plugins/setup-registry.test.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,10 +378,85 @@ describe("setup-registry getJiti", () => {
378378
cliBackends: [],
379379
configMigrations: [],
380380
autoEnableProbes: [],
381+
diagnostics: [],
381382
});
382383
expect(mocks.createJiti).not.toHaveBeenCalled();
383384
});
384385

386+
it("reports setup descriptor drift without rejecting runtime registrations", () => {
387+
const pluginRoot = makeTempDir();
388+
fs.writeFileSync(path.join(pluginRoot, "setup-api.js"), "export default {};\n", "utf-8");
389+
mocks.loadPluginManifestRegistry.mockReturnValue({
390+
plugins: [
391+
{
392+
id: "openai",
393+
rootDir: pluginRoot,
394+
setup: {
395+
providers: [{ id: "openai" }],
396+
cliBackends: ["codex-cli"],
397+
requiresRuntime: true,
398+
},
399+
},
400+
],
401+
diagnostics: [],
402+
});
403+
mocks.createJiti.mockImplementation(() => {
404+
return () => ({
405+
default: {
406+
register(api: {
407+
registerProvider: (provider: { id: string; label: string; auth: [] }) => void;
408+
registerCliBackend: (backend: { id: string; config: { command: string } }) => void;
409+
}) {
410+
api.registerProvider({
411+
id: "anthropic",
412+
label: "Anthropic",
413+
auth: [],
414+
});
415+
api.registerCliBackend({
416+
id: "claude-cli",
417+
config: { command: "claude" },
418+
});
419+
},
420+
},
421+
});
422+
});
423+
424+
const registry = resolvePluginSetupRegistry({ env: {} });
425+
426+
expect(registry.providers.map((entry) => entry.provider.id)).toEqual(["anthropic"]);
427+
expect(registry.cliBackends.map((entry) => entry.backend.id)).toEqual(["claude-cli"]);
428+
expect(registry.diagnostics).toEqual([
429+
expect.objectContaining({
430+
pluginId: "openai",
431+
code: "setup-descriptor-provider-missing-runtime",
432+
declaredId: "openai",
433+
}),
434+
expect.objectContaining({
435+
pluginId: "openai",
436+
code: "setup-descriptor-provider-runtime-undeclared",
437+
runtimeId: "anthropic",
438+
}),
439+
expect.objectContaining({
440+
pluginId: "openai",
441+
code: "setup-descriptor-cli-backend-missing-runtime",
442+
declaredId: "codex-cli",
443+
}),
444+
expect.objectContaining({
445+
pluginId: "openai",
446+
code: "setup-descriptor-cli-backend-runtime-undeclared",
447+
runtimeId: "claude-cli",
448+
}),
449+
]);
450+
});
451+
452+
it("does not report drift when setup descriptors match runtime registrations", () => {
453+
mockOpenAiCliBackendRegistration({
454+
requiresRuntime: true,
455+
});
456+
457+
expect(resolvePluginSetupRegistry({ env: {} }).diagnostics).toEqual([]);
458+
});
459+
385460
it("does not load setup-api modules from the current working directory", () => {
386461
const pluginRoot = makeTempDir();
387462
const workspaceRoot = makeTempDir();

src/plugins/setup-registry.ts

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,26 @@ type SetupAutoEnableProbeEntry = {
4646
probe: PluginSetupAutoEnableProbe;
4747
};
4848

49+
export type PluginSetupRegistryDiagnosticCode =
50+
| "setup-descriptor-provider-missing-runtime"
51+
| "setup-descriptor-provider-runtime-undeclared"
52+
| "setup-descriptor-cli-backend-missing-runtime"
53+
| "setup-descriptor-cli-backend-runtime-undeclared";
54+
55+
export type PluginSetupRegistryDiagnostic = {
56+
pluginId: string;
57+
code: PluginSetupRegistryDiagnosticCode;
58+
declaredId?: string;
59+
runtimeId?: string;
60+
message: string;
61+
};
62+
4963
type PluginSetupRegistry = {
5064
providers: SetupProviderEntry[];
5165
cliBackends: SetupCliBackendEntry[];
5266
configMigrations: SetupConfigMigrationEntry[];
5367
autoEnableProbes: SetupAutoEnableProbeEntry[];
68+
diagnostics: PluginSetupRegistryDiagnostic[];
5469
};
5570

5671
type SetupAutoEnableReason = {
@@ -372,6 +387,75 @@ function findUniqueSetupManifestOwner(params: {
372387
return matches.length === 1 ? matches[0] : undefined;
373388
}
374389

390+
function mapNormalizedIds(ids: readonly string[]): Map<string, string> {
391+
const mapped = new Map<string, string>();
392+
for (const id of ids) {
393+
const normalized = normalizeProviderId(id);
394+
if (!normalized || mapped.has(normalized)) {
395+
continue;
396+
}
397+
mapped.set(normalized, id);
398+
}
399+
return mapped;
400+
}
401+
402+
function pushSetupDescriptorDriftDiagnostics(params: {
403+
record: PluginManifestRecord;
404+
providers: readonly ProviderPlugin[];
405+
cliBackends: readonly CliBackendPlugin[];
406+
diagnostics: PluginSetupRegistryDiagnostic[];
407+
}): void {
408+
const declaredProviderIds = params.record.setup?.providers?.map((entry) => entry.id);
409+
if (declaredProviderIds) {
410+
for (const declaredId of declaredProviderIds) {
411+
if (!params.providers.some((provider) => matchesProvider(provider, declaredId))) {
412+
params.diagnostics.push({
413+
pluginId: params.record.id,
414+
code: "setup-descriptor-provider-missing-runtime",
415+
declaredId,
416+
message: `setup.providers declares "${declaredId}" but setup runtime did not register a matching provider.`,
417+
});
418+
}
419+
}
420+
for (const provider of params.providers) {
421+
if (!declaredProviderIds.some((declaredId) => matchesProvider(provider, declaredId))) {
422+
params.diagnostics.push({
423+
pluginId: params.record.id,
424+
code: "setup-descriptor-provider-runtime-undeclared",
425+
runtimeId: provider.id,
426+
message: `setup runtime registered provider "${provider.id}" but setup.providers does not declare it.`,
427+
});
428+
}
429+
}
430+
}
431+
432+
const declaredCliBackendIds = params.record.setup?.cliBackends;
433+
if (declaredCliBackendIds) {
434+
const declaredCliBackends = mapNormalizedIds(declaredCliBackendIds);
435+
const runtimeCliBackends = mapNormalizedIds(params.cliBackends.map((backend) => backend.id));
436+
for (const [normalized, declaredId] of declaredCliBackends) {
437+
if (!runtimeCliBackends.has(normalized)) {
438+
params.diagnostics.push({
439+
pluginId: params.record.id,
440+
code: "setup-descriptor-cli-backend-missing-runtime",
441+
declaredId,
442+
message: `setup.cliBackends declares "${declaredId}" but setup runtime did not register a matching CLI backend.`,
443+
});
444+
}
445+
}
446+
for (const [normalized, runtimeId] of runtimeCliBackends) {
447+
if (!declaredCliBackends.has(normalized)) {
448+
params.diagnostics.push({
449+
pluginId: params.record.id,
450+
code: "setup-descriptor-cli-backend-runtime-undeclared",
451+
runtimeId,
452+
message: `setup runtime registered CLI backend "${runtimeId}" but setup.cliBackends does not declare it.`,
453+
});
454+
}
455+
}
456+
}
457+
}
458+
375459
export function resolvePluginSetupRegistry(params?: {
376460
workspaceDir?: string;
377461
env?: NodeJS.ProcessEnv;
@@ -397,6 +481,7 @@ export function resolvePluginSetupRegistry(params?: {
397481
cliBackends: [],
398482
configMigrations: [],
399483
autoEnableProbes: [],
484+
diagnostics: [],
400485
} satisfies PluginSetupRegistry;
401486
setCachedSetupValue(setupRegistryCache, cacheKey, empty);
402487
return empty;
@@ -406,6 +491,7 @@ export function resolvePluginSetupRegistry(params?: {
406491
const cliBackends: SetupCliBackendEntry[] = [];
407492
const configMigrations: SetupConfigMigrationEntry[] = [];
408493
const autoEnableProbes: SetupAutoEnableProbeEntry[] = [];
494+
const diagnostics: PluginSetupRegistryDiagnostic[] = [];
409495
const providerKeys = new Set<string>();
410496
const cliBackendKeys = new Set<string>();
411497

@@ -423,6 +509,8 @@ export function resolvePluginSetupRegistry(params?: {
423509
continue;
424510
}
425511

512+
const recordProviders: ProviderPlugin[] = [];
513+
const recordCliBackends: CliBackendPlugin[] = [];
426514
const api = buildSetupPluginApi({
427515
record,
428516
setupSource: setupRegistration.setupSource,
@@ -437,6 +525,7 @@ export function resolvePluginSetupRegistry(params?: {
437525
pluginId: record.id,
438526
provider,
439527
});
528+
recordProviders.push(provider);
440529
},
441530
registerCliBackend(backend) {
442531
const key = `${record.id}:${normalizeProviderId(backend.id)}`;
@@ -448,6 +537,7 @@ export function resolvePluginSetupRegistry(params?: {
448537
pluginId: record.id,
449538
backend,
450539
});
540+
recordCliBackends.push(backend);
451541
},
452542
registerConfigMigration(migrate) {
453543
configMigrations.push({
@@ -473,13 +563,20 @@ export function resolvePluginSetupRegistry(params?: {
473563
} catch {
474564
continue;
475565
}
566+
pushSetupDescriptorDriftDiagnostics({
567+
record,
568+
providers: recordProviders,
569+
cliBackends: recordCliBackends,
570+
diagnostics,
571+
});
476572
}
477573

478574
const registry = {
479575
providers,
480576
cliBackends,
481577
configMigrations,
482578
autoEnableProbes,
579+
diagnostics,
483580
} satisfies PluginSetupRegistry;
484581
setCachedSetupValue(setupRegistryCache, cacheKey, registry);
485582
return registry;

0 commit comments

Comments
 (0)