fix(plugins): honor descriptor-only setup flag

Honor explicit setup.requiresRuntime: false as a descriptor-only setup contract while preserving omitted values as the legacy setup-api fallback path.
This commit is contained in:
Vincent Koc
2026-04-24 11:02:38 -07:00
committed by GitHub
parent a16f8dff15
commit 7418adf875
5 changed files with 49 additions and 4 deletions

View File

@@ -7,6 +7,7 @@ Docs: https://docs.openclaw.ai
### Changes
- 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.
- 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.

View File

@@ -71,10 +71,12 @@ or fallback behavior without changing runtime loading semantics.
Setup discovery now prefers descriptor-owned ids such as `setup.providers` and
`setup.cliBackends` to narrow candidate plugins before it falls back to
`setup-api` for plugins that still need setup-time runtime hooks. 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.
`setup-api` for plugins that still need setup-time runtime hooks. Explicit
`setup.requiresRuntime: false` is a descriptor-only cutoff; omitted
`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.
### What the loader caches

View File

@@ -327,6 +327,12 @@ narrows the candidate plugin and setup still needs richer setup-time runtime
hooks, set `requiresRuntime: true` and keep `setup-api` in place as the
fallback execution path.
Set `requiresRuntime: false` only when those descriptors are sufficient for the
setup surface. OpenClaw treats explicit `false` as a descriptor-only contract
and will not execute `setup-api` for setup lookup. Omitted `requiresRuntime`
keeps legacy fallback behavior so existing plugins that added descriptors
without the flag do not break.
Because setup lookup can execute plugin-owned `setup-api` code, normalized
`setup.providers[].id` and `setup.cliBackends[]` values must stay unique across
discovered plugins. Ambiguous ownership fails closed instead of picking a

View File

@@ -349,6 +349,39 @@ describe("setup-registry getJiti", () => {
expect(mocks.createJiti.mock.calls[0]?.[0]).toBe(path.join(pluginRoot, "setup-api.js"));
});
it("treats explicit descriptor-only setup as a runtime cutoff", () => {
const pluginRoot = makeTempDir();
fs.writeFileSync(
path.join(pluginRoot, "setup-api.js"),
"export default { register(api) { api.registerProvider({ id: 'openai', label: 'OpenAI', auth: [] }); api.registerCliBackend({ id: 'codex-cli', config: { command: 'codex' } }); } };\n",
"utf-8",
);
mocks.loadPluginManifestRegistry.mockReturnValue({
plugins: [
{
id: "openai",
rootDir: pluginRoot,
setup: {
providers: [{ id: "openai" }],
cliBackends: ["codex-cli"],
requiresRuntime: false,
},
},
],
diagnostics: [],
});
expect(resolvePluginSetupProvider({ provider: "openai", env: {} })).toBeUndefined();
expect(resolvePluginSetupCliBackend({ backend: "codex-cli", env: {} })).toBeUndefined();
expect(resolvePluginSetupRegistry({ env: {} })).toEqual({
providers: [],
cliBackends: [],
configMigrations: [],
autoEnableProbes: [],
});
expect(mocks.createJiti).not.toHaveBeenCalled();
});
it("does not load setup-api modules from the current working directory", () => {
const pluginRoot = makeTempDir();
const workspaceRoot = makeTempDir();

View File

@@ -272,6 +272,9 @@ function resolveSetupRegistration(record: PluginManifestRecord): {
setupSource: string;
register: (api: ReturnType<typeof buildPluginApi>) => void | Promise<void>;
} | null {
if (record.setup?.requiresRuntime === false) {
return null;
}
const setupSource = record.setupSource ?? resolveSetupApiPath(record.rootDir);
if (!setupSource) {
return null;