mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 09:30:43 +00:00
feat: warn on implicit startup plugin compatibility
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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: [],
|
||||
|
||||
Reference in New Issue
Block a user