fix(plugin-sdk): restore root diagnostic compat

This commit is contained in:
Vincent Koc
2026-03-20 09:25:13 -07:00
parent 50ce9ac1c6
commit dbc9d3dd70
9 changed files with 82 additions and 1 deletions

View File

@@ -42,7 +42,7 @@ const exportedNames = exportMatch[1]
const exportSet = new Set(exportedNames);
const requiredRuntimeShimEntries = ["root-alias.cjs"];
const requiredRuntimeShimEntries = ["compat.js", "root-alias.cjs"];
// Critical functions that channel extension plugins import from openclaw/plugin-sdk.
// If any of these are missing, plugins will fail at runtime with:
@@ -65,6 +65,7 @@ const requiredExports = [
"resolveChannelMediaMaxBytes",
"warnMissingProviderGroupPolicyFallbackOnce",
"emptyPluginConfigSchema",
"onDiagnosticEvent",
"normalizePluginHttpPath",
"registerPluginHttpRoute",
"DEFAULT_ACCOUNT_ID",

View File

@@ -21,6 +21,7 @@ const requiredPathGroups = [
["dist/index.js", "dist/index.mjs"],
["dist/entry.js", "dist/entry.mjs"],
...listPluginSdkDistArtifacts(),
"dist/plugin-sdk/compat.js",
"dist/plugin-sdk/root-alias.cjs",
"dist/build-info.json",
];
@@ -228,6 +229,7 @@ const requiredPluginSdkExports = [
"resolveChannelMediaMaxBytes",
"warnMissingProviderGroupPolicyFallbackOnce",
"emptyPluginConfigSchema",
"onDiagnosticEvent",
"normalizePluginHttpPath",
"registerPluginHttpRoute",
"DEFAULT_ACCOUNT_ID",

View File

@@ -36,6 +36,7 @@ describe("tsdown config", () => {
expect.arrayContaining([
"index",
"plugins/runtime/index",
"plugin-sdk/compat",
"plugin-sdk/index",
"extensions/openai/index",
"bundled/boot-md/handler",

View File

@@ -20,6 +20,8 @@ if (shouldWarnCompatImport) {
export { emptyPluginConfigSchema } from "../plugins/config-schema.js";
export { resolveControlCommandGate } from "../channels/command-gating.js";
export { delegateCompactionToRuntime } from "../context-engine/delegate.js";
export type { DiagnosticEventPayload } from "../infra/diagnostic-events.js";
export { onDiagnosticEvent } from "../infra/diagnostic-events.js";
export { createAccountStatusSink } from "./channel-lifecycle.js";
export { createPluginRuntimeStore } from "./runtime-store.js";

View File

@@ -50,9 +50,11 @@ describe("plugin-sdk exports", () => {
it("keeps the root runtime surface intentionally small", () => {
expect(typeof sdk.emptyPluginConfigSchema).toBe("function");
expect(typeof sdk.delegateCompactionToRuntime).toBe("function");
expect(typeof sdk.onDiagnosticEvent).toBe("function");
expect(Object.prototype.hasOwnProperty.call(sdk, "resolveControlCommandGate")).toBe(false);
expect(Object.prototype.hasOwnProperty.call(sdk, "buildAgentSessionKey")).toBe(false);
expect(Object.prototype.hasOwnProperty.call(sdk, "isDangerousNameMatchingEnabled")).toBe(false);
expect(Object.prototype.hasOwnProperty.call(sdk, "emitDiagnosticEvent")).toBe(false);
});
it("keeps package.json plugin-sdk exports synced with the manifest", async () => {

View File

@@ -64,7 +64,9 @@ export type { HookEntry } from "../hooks/types.js";
export type { ReplyPayload } from "../auto-reply/types.js";
export type { WizardPrompter } from "../wizard/prompts.js";
export type { ContextEngineFactory } from "../context-engine/registry.js";
export type { DiagnosticEventPayload } from "../infra/diagnostic-events.js";
export { emptyPluginConfigSchema } from "../plugins/config-schema.js";
export { registerContextEngine } from "../context-engine/registry.js";
export { delegateCompactionToRuntime } from "../context-engine/delegate.js";
export { onDiagnosticEvent } from "../infra/diagnostic-events.js";

View File

@@ -5,6 +5,7 @@ const fs = require("node:fs");
let monolithicSdk = null;
const jitiLoaders = new Map();
const pluginSdkSubpathsCache = new Map();
function emptyPluginConfigSchema() {
function error(message) {
@@ -61,6 +62,49 @@ function resolveControlCommandGate(params) {
return { commandAuthorized, shouldBlock };
}
function getPackageRoot() {
return path.resolve(__dirname, "..", "..");
}
function listPluginSdkExportedSubpaths() {
const packageRoot = getPackageRoot();
if (pluginSdkSubpathsCache.has(packageRoot)) {
return pluginSdkSubpathsCache.get(packageRoot);
}
let subpaths = [];
try {
const packageJsonPath = path.join(packageRoot, "package.json");
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
subpaths = Object.keys(packageJson.exports ?? {})
.filter((key) => key.startsWith("./plugin-sdk/"))
.map((key) => key.slice("./plugin-sdk/".length));
} catch {
subpaths = [];
}
pluginSdkSubpathsCache.set(packageRoot, subpaths);
return subpaths;
}
function buildPluginSdkAliasMap(useDist) {
const packageRoot = getPackageRoot();
const pluginSdkDir = path.join(packageRoot, useDist ? "dist" : "src", "plugin-sdk");
const ext = useDist ? ".js" : ".ts";
const aliasMap = {
"openclaw/plugin-sdk": __filename,
};
for (const subpath of listPluginSdkExportedSubpaths()) {
const candidate = path.join(pluginSdkDir, `${subpath}${ext}`);
if (fs.existsSync(candidate)) {
aliasMap[`openclaw/plugin-sdk/${subpath}`] = candidate;
}
}
return aliasMap;
}
function getJiti(tryNative) {
if (jitiLoaders.has(tryNative)) {
return jitiLoaders.get(tryNative);
@@ -68,6 +112,7 @@ function getJiti(tryNative) {
const { createJiti } = require("jiti");
const jitiLoader = createJiti(__filename, {
alias: buildPluginSdkAliasMap(tryNative),
interopDefault: true,
// Prefer Node's native sync ESM loader for built dist/plugin-sdk/*.js files
// so local plugins do not create a second transpiled OpenClaw core graph.

View File

@@ -48,6 +48,12 @@ function loadRootAliasWithStubs(options?: {
}
if (id === "node:fs") {
return {
readFileSync: () =>
JSON.stringify({
exports: {
"./plugin-sdk/group-access": { default: "./dist/plugin-sdk/group-access.js" },
},
}),
existsSync: () => options?.distExists ?? false,
};
}
@@ -164,8 +170,23 @@ describe("plugin-sdk root alias", () => {
expect("delegateCompactionToRuntime" in lazyRootSdk).toBe(true);
});
it("forwards onDiagnosticEvent through the compat-backed root alias", () => {
const onDiagnosticEvent = () => () => undefined;
const lazyModule = loadRootAliasWithStubs({
monolithicExports: {
onDiagnosticEvent,
},
});
const lazyRootSdk = lazyModule.moduleExports;
expect(typeof lazyRootSdk.onDiagnosticEvent).toBe("function");
expect(lazyRootSdk.onDiagnosticEvent).toBe(onDiagnosticEvent);
expect("onDiagnosticEvent" in lazyRootSdk).toBe(true);
});
it("loads legacy root exports through the merged root wrapper", { timeout: 240_000 }, () => {
expect(typeof rootSdk.resolveControlCommandGate).toBe("function");
expect(typeof rootSdk.onDiagnosticEvent).toBe("function");
expect(typeof rootSdk.default).toBe("object");
expect(rootSdk.default).toBe(rootSdk);
expect(rootSdk.__esModule).toBe(true);
@@ -173,9 +194,12 @@ describe("plugin-sdk root alias", () => {
it("preserves reflection semantics for lazily resolved exports", { timeout: 240_000 }, () => {
expect("resolveControlCommandGate" in rootSdk).toBe(true);
expect("onDiagnosticEvent" in rootSdk).toBe(true);
const keys = Object.keys(rootSdk);
expect(keys).toContain("resolveControlCommandGate");
expect(keys).toContain("onDiagnosticEvent");
const descriptor = Object.getOwnPropertyDescriptor(rootSdk, "resolveControlCommandGate");
expect(descriptor).toBeDefined();
expect(Object.getOwnPropertyDescriptor(rootSdk, "onDiagnosticEvent")).toBeDefined();
});
});

View File

@@ -186,6 +186,8 @@ const coreDistEntries = buildCoreDistEntries();
function buildUnifiedDistEntries(): Record<string, string> {
return {
...coreDistEntries,
// Internal compat artifact for the root-alias.cjs lazy loader.
"plugin-sdk/compat": "src/plugin-sdk/compat.ts",
...Object.fromEntries(
Object.entries(buildPluginSdkEntrySources()).map(([entry, source]) => [
`plugin-sdk/${entry}`,