feat: warn on implicit startup plugin compatibility

This commit is contained in:
Shakker
2026-04-28 05:56:23 +01:00
parent f7e942f571
commit d062f8130b
7 changed files with 165 additions and 19 deletions

View File

@@ -28,6 +28,7 @@ Docs: https://docs.openclaw.ai
- Channels/Yuanbao: register the Tencent Yuanbao external channel plugin (`openclaw-plugin-yuanbao`) in the official channel catalog, contract suites, and community plugin docs, with a new `docs/channels/yuanbao.md` quick-start guide for WebSocket bot DMs and group chats. (#72756) Thanks @loongfay.
- Channels/QQBot: add full group chat support (history tracking, @-mention gating, activation modes, per-group config, FIFO message queue with deliver debounce), C2C `stream_messages` streaming with a `StreamingController` lifecycle manager, unified `sendMedia` with chunked upload for large files, and refactor the engine into pipeline stages, focused outbound submodules, builtin slash-command modules, and explicit DI ports via `createEngineAdapters()`. (#70624) Thanks @cxyhhhhh.
- Plugins/startup: migrate bundled plugin manifests to explicit `activation.onStartup` declarations so Gateway startup imports only the bundled plugins that intentionally register startup-time runtime surfaces. Thanks @shakkernerd.
- Plugins/startup: add plugin compatibility warnings for deprecated implicit startup loading so authors can migrate to explicit `activation.onStartup` metadata. Thanks @shakkernerd.
- Plugins/runtime: load bundled agent tool-result middleware from manifest contracts on demand so tokenjuice stays startup-lazy without losing Pi/Codex tool-output compaction. Thanks @shakkernerd.
- Plugins/startup: add explicit `activation.onStartup` metadata so plugins can declare Gateway startup import behavior while the deprecated implicit sidecar fallback remains for legacy plugins. Thanks @shakkernerd.
- Gateway/startup: reuse lookup-table plugin manifests when loading startup plugins so Gateway boot avoids rebuilding plugin discovery and manifest metadata. Thanks @shakkernerd.

View File

@@ -135,6 +135,8 @@ Current compatibility records include:
- legacy channel route key and comparable-target helper aliases while plugins
move to `openclaw/plugin-sdk/channel-route`
- activation hints that are being replaced by manifest contribution ownership
- deprecated implicit startup sidecar loading for plugins that have not declared
`activation.onStartup`
- `setup-api` runtime fallback while setup descriptors move to cold
`setup.requiresRuntime: false` metadata
- provider `discovery` hooks while provider catalog hooks move to

View File

@@ -264,7 +264,9 @@ run during Gateway startup. Set it to `false` when the plugin is inert at
startup and should load only from narrower triggers. Omitting `onStartup` keeps
the deprecated legacy implicit startup sidecar fallback for plugins with no
static capability metadata; future versions may stop startup-loading those
plugins unless they declare `activation.onStartup: true`.
plugins unless they declare `activation.onStartup: true`. Plugin status and
compatibility reports warn with `legacy-implicit-startup-sidecar` when a plugin
still relies on that fallback.
```json
{

View File

@@ -9,6 +9,7 @@ import type {
AgentToolResultMiddlewareRuntime,
} from "./agent-tool-result-middleware-types.js";
import type { CodexAppServerExtensionFactory } from "./codex-app-server-extension-types.js";
import type { PluginCompatCode } from "./compat/registry.js";
import type { PluginActivationSource } from "./config-state.js";
import type {
PluginAgentEventSubscriptionRegistration,
@@ -328,6 +329,7 @@ export type PluginRecord = {
explicitlyEnabled?: boolean;
activated?: boolean;
imported?: boolean;
compat?: readonly PluginCompatCode[];
activationSource?: PluginActivationSource;
activationReason?: string;
status: "loaded" | "disabled" | "error";

View File

@@ -5,29 +5,43 @@ import type { PluginHookName } from "./types.js";
export const LEGACY_BEFORE_AGENT_START_MESSAGE =
"still uses legacy before_agent_start; keep regression coverage on this plugin, and prefer before_model_resolve/before_prompt_build for new work.";
export const LEGACY_IMPLICIT_STARTUP_SIDECAR_MESSAGE =
"relies on deprecated implicit startup loading; add activation.onStartup: true for startup work or activation.onStartup: false for startup-lazy plugins.";
export const HOOK_ONLY_MESSAGE =
"is hook-only. This remains a supported compatibility path, but it has not migrated to explicit capability registration yet.";
export function createCompatibilityNotice(
params: Pick<PluginCompatibilityNotice, "pluginId" | "code">,
): PluginCompatibilityNotice {
if (params.code === "legacy-before-agent-start") {
return {
pluginId: params.pluginId,
code: params.code,
compatCode: "legacy-before-agent-start",
severity: "warn",
message: LEGACY_BEFORE_AGENT_START_MESSAGE,
};
switch (params.code) {
case "legacy-before-agent-start":
return {
pluginId: params.pluginId,
code: params.code,
compatCode: "legacy-before-agent-start",
severity: "warn",
message: LEGACY_BEFORE_AGENT_START_MESSAGE,
};
case "legacy-implicit-startup-sidecar":
return {
pluginId: params.pluginId,
code: params.code,
compatCode: "legacy-implicit-startup-sidecar",
severity: "warn",
message: LEGACY_IMPLICIT_STARTUP_SIDECAR_MESSAGE,
};
case "hook-only":
return {
pluginId: params.pluginId,
code: params.code,
compatCode: "hook-only-plugin-shape",
severity: "info",
message: HOOK_ONLY_MESSAGE,
};
}
return {
pluginId: params.pluginId,
code: params.code,
compatCode: "hook-only-plugin-shape",
severity: "info",
message: HOOK_ONLY_MESSAGE,
};
const unsupportedCode: never = params.code;
void unsupportedCode;
throw new Error("unsupported compatibility notice code");
}
export function createPluginRecord(

View File

@@ -7,11 +7,14 @@ import {
createTypedHook,
HOOK_ONLY_MESSAGE,
LEGACY_BEFORE_AGENT_START_MESSAGE,
LEGACY_IMPLICIT_STARTUP_SIDECAR_MESSAGE,
} from "./status.test-helpers.js";
const loadConfigMock = vi.fn();
const loadOpenClawPluginsMock = vi.fn();
const loadPluginMetadataRegistrySnapshotMock = vi.fn();
const loadPluginRegistrySnapshotWithMetadataMock = vi.fn();
const loadPluginManifestRegistryForInstalledIndexMock = vi.fn();
const applyPluginAutoEnableMock = vi.fn();
const resolveBundledProviderCompatPluginIdsMock = vi.fn();
const withBundledPluginAllowlistCompatMock = vi.fn();
@@ -19,6 +22,7 @@ const withBundledPluginEnablementCompatMock = vi.fn();
const listImportedBundledPluginFacadeIdsMock = vi.fn();
const listImportedRuntimePluginIdsMock = vi.fn();
let buildPluginSnapshotReport: typeof import("./status.js").buildPluginSnapshotReport;
let buildPluginRegistrySnapshotReport: typeof import("./status.js").buildPluginRegistrySnapshotReport;
let buildPluginDiagnosticsReport: typeof import("./status.js").buildPluginDiagnosticsReport;
let buildPluginInspectReport: typeof import("./status.js").buildPluginInspectReport;
let buildAllPluginInspectReports: typeof import("./status.js").buildAllPluginInspectReports;
@@ -45,6 +49,16 @@ vi.mock("./runtime/metadata-registry-loader.js", () => ({
loadPluginMetadataRegistrySnapshotMock(...args),
}));
vi.mock("./plugin-registry.js", () => ({
loadPluginRegistrySnapshotWithMetadata: (...args: unknown[]) =>
loadPluginRegistrySnapshotWithMetadataMock(...args),
}));
vi.mock("./manifest-registry-installed.js", () => ({
loadPluginManifestRegistryForInstalledIndex: (...args: unknown[]) =>
loadPluginManifestRegistryForInstalledIndexMock(...args),
}));
vi.mock("./providers.js", () => ({
resolveBundledProviderCompatPluginIds: (...args: unknown[]) =>
resolveBundledProviderCompatPluginIdsMock(...args),
@@ -95,6 +109,23 @@ function setSinglePluginLoadResult(
});
}
function createInstalledPluginIndexSnapshot(
plugins: Array<Record<string, unknown>>,
): Record<string, unknown> {
return {
version: 1,
warning: "test",
hostContractVersion: "test",
compatRegistryVersion: "test",
migrationVersion: 1,
policyHash: "test",
generatedAtMs: 0,
installRecords: {},
plugins,
diagnostics: [],
};
}
function expectInspectReport(
pluginId: string,
): NonNullable<ReturnType<typeof buildPluginInspectReport>> {
@@ -321,6 +352,7 @@ describe("plugin status reports", () => {
buildPluginDiagnosticsReport,
buildPluginCompatibilityWarnings,
buildPluginInspectReport,
buildPluginRegistrySnapshotReport,
buildPluginSnapshotReport,
formatPluginCompatibilityNotice,
summarizePluginCompatibility,
@@ -331,6 +363,8 @@ describe("plugin status reports", () => {
loadConfigMock.mockReset();
loadOpenClawPluginsMock.mockReset();
loadPluginMetadataRegistrySnapshotMock.mockReset();
loadPluginRegistrySnapshotWithMetadataMock.mockReset();
loadPluginManifestRegistryForInstalledIndexMock.mockReset();
applyPluginAutoEnableMock.mockReset();
resolveBundledProviderCompatPluginIdsMock.mockReset();
withBundledPluginAllowlistCompatMock.mockReset();
@@ -338,6 +372,15 @@ describe("plugin status reports", () => {
listImportedBundledPluginFacadeIdsMock.mockReset();
listImportedRuntimePluginIdsMock.mockReset();
loadConfigMock.mockReturnValue({});
loadPluginRegistrySnapshotWithMetadataMock.mockReturnValue({
snapshot: createInstalledPluginIndexSnapshot([]),
source: "derived",
diagnostics: [],
});
loadPluginManifestRegistryForInstalledIndexMock.mockReturnValue({
plugins: [],
diagnostics: [],
});
applyPluginAutoEnableMock.mockImplementation((params: { config: unknown }) => ({
config: params.config,
changes: [],
@@ -393,6 +436,41 @@ describe("plugin status reports", () => {
});
});
it("carries installed-index compatibility metadata into registry snapshot reports", () => {
loadPluginRegistrySnapshotWithMetadataMock.mockReturnValue({
snapshot: createInstalledPluginIndexSnapshot([
{
pluginId: "legacy-sidecar",
manifestPath: "/tmp/legacy-sidecar/openclaw.plugin.json",
manifestHash: "manifest-hash",
rootDir: "/tmp/legacy-sidecar",
origin: "workspace",
enabled: true,
startup: {
sidecar: true,
memory: false,
deferConfiguredChannelFullLoadUntilAfterListen: false,
agentHarnesses: [],
},
compat: ["legacy-implicit-startup-sidecar"],
},
]),
source: "derived",
diagnostics: [],
});
loadPluginManifestRegistryForInstalledIndexMock.mockReturnValue({
plugins: [{ id: "legacy-sidecar", name: "Legacy Sidecar" }],
diagnostics: [],
});
const report = buildPluginRegistrySnapshotReport({ config: {} });
expect(report.plugins[0]).toMatchObject({
id: "legacy-sidecar",
compat: ["legacy-implicit-startup-sidecar"],
});
});
it("uses a metadata snapshot load for snapshot reports", () => {
buildPluginSnapshotReport({ config: {}, workspaceDir: "/workspace" });
@@ -755,6 +833,38 @@ describe("plugin status reports", () => {
});
});
it("builds compatibility warnings for deprecated implicit startup sidecar metadata", () => {
setSinglePluginLoadResult(
createPluginRecord({
id: "legacy-sidecar",
name: "Legacy Sidecar",
compat: ["legacy-implicit-startup-sidecar"],
}),
);
expectCompatibilityOutput({
notices: [
createCompatibilityNotice({
pluginId: "legacy-sidecar",
code: "legacy-implicit-startup-sidecar",
}),
],
warnings: [`legacy-sidecar ${LEGACY_IMPLICIT_STARTUP_SIDECAR_MESSAGE}`],
});
});
it("does not warn when explicit startup-lazy metadata avoids legacy startup compatibility", () => {
setSinglePluginLoadResult(
createPluginRecord({
id: "modern-startup-lazy",
name: "Modern Startup Lazy",
compat: [],
}),
);
expectNoCompatibilityWarnings();
});
it("returns no compatibility warnings for modern capability plugins", () => {
setSinglePluginLoadResult(
createPluginRecord({
@@ -819,10 +929,14 @@ describe("plugin status reports", () => {
expect(
summarizePluginCompatibility([
notice,
createCompatibilityNotice({
pluginId: "legacy-plugin",
code: "legacy-implicit-startup-sidecar",
}),
createCompatibilityNotice({ pluginId: "legacy-plugin", code: "hook-only" }),
]),
).toEqual({
noticeCount: 2,
noticeCount: 3,
pluginCount: 1,
});
});

View File

@@ -50,7 +50,7 @@ export type { PluginCapabilityKind, PluginInspectShape } from "./inspect-shape.j
export type PluginCompatibilityNotice = {
pluginId: string;
code: "legacy-before-agent-start" | "hook-only";
code: "legacy-before-agent-start" | "legacy-implicit-startup-sidecar" | "hook-only";
compatCode: PluginCompatCode;
severity: "warn" | "info";
message: string;
@@ -121,6 +121,16 @@ function buildCompatibilityNoticesForInspect(
"still uses legacy before_agent_start; keep regression coverage on this plugin, and prefer before_model_resolve/before_prompt_build for new work.",
});
}
if (inspect.plugin.compat?.includes("legacy-implicit-startup-sidecar")) {
warnings.push({
pluginId: inspect.plugin.id,
code: "legacy-implicit-startup-sidecar",
compatCode: "legacy-implicit-startup-sidecar",
severity: "warn",
message:
"relies on deprecated implicit startup loading; add activation.onStartup: true for startup work or activation.onStartup: false for startup-lazy plugins.",
});
}
if (inspect.shape === "hook-only") {
warnings.push({
pluginId: inspect.plugin.id,
@@ -177,6 +187,7 @@ function buildPluginRecordFromInstalledIndex(
rootDir: plugin.rootDir,
origin: plugin.origin,
enabled: plugin.enabled,
compat: plugin.compat,
syntheticAuthRefs: [...(plugin.syntheticAuthRefs ?? manifest?.syntheticAuthRefs ?? [])],
status: plugin.enabled ? "loaded" : "disabled",
toolNames: [],