refactor: add xai plugin-sdk boundary canary (#61548)

* docs: plan real plugin-sdk workspace rollout

* build: add xai plugin-sdk boundary canary

* build: generate plugin-sdk package types

* build: hide plugin-sdk core export

* build: alias scoped plugin-sdk runtime imports

* build: repair plugin-sdk boundary drift

* fix(plugins): remove duplicated plugin-sdk entrypoints

* test(plugins): make tsc boundary canary portable

---------

Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
This commit is contained in:
Harold Hunt
2026-04-06 09:13:11 -04:00
committed by GitHub
parent 0430bab070
commit 0bd0097557
53 changed files with 952 additions and 51 deletions

View File

@@ -15,6 +15,11 @@ export type {
export type { OpenClawConfig } from "../config/config.js";
export { describeFailoverError, isFailoverError } from "../agents/failover-error.js";
export {
buildNoCapabilityModelConfiguredMessage,
resolveCapabilityModelCandidates,
throwCapabilityGenerationFailure,
} from "../media-generation/runtime-shared.js";
export {
resolveAgentModelFallbackValues,
resolveAgentModelPrimaryValue,

View File

@@ -20,6 +20,11 @@ export type {
export type { OpenClawConfig } from "../config/config.js";
export { describeFailoverError, isFailoverError } from "../agents/failover-error.js";
export {
buildNoCapabilityModelConfiguredMessage,
resolveCapabilityModelCandidates,
throwCapabilityGenerationFailure,
} from "../media-generation/runtime-shared.js";
export {
resolveAgentModelFallbackValues,
resolveAgentModelPrimaryValue,

View File

@@ -33,7 +33,11 @@ function applyVitestCapabilityAliasOverrides(params: {
return params.aliasMap;
}
const { ["openclaw/plugin-sdk"]: _ignoredRootAlias, ...scopedAliasMap } = params.aliasMap;
const {
["openclaw/plugin-sdk"]: _ignoredLegacyRootAlias,
["@openclaw/plugin-sdk"]: _ignoredScopedRootAlias,
...scopedAliasMap
} = params.aliasMap;
return {
...scopedAliasMap,
// Capability contract loads only need a narrow SDK slice. Keep those
@@ -42,18 +46,33 @@ function applyVitestCapabilityAliasOverrides(params: {
"openclaw/plugin-sdk/llm-task": fileURLToPath(
new URL("./capability-runtime-vitest-shims/llm-task.ts", import.meta.url),
),
"@openclaw/plugin-sdk/llm-task": fileURLToPath(
new URL("./capability-runtime-vitest-shims/llm-task.ts", import.meta.url),
),
"openclaw/plugin-sdk/config-runtime": fileURLToPath(
new URL("./capability-runtime-vitest-shims/config-runtime.ts", import.meta.url),
),
"@openclaw/plugin-sdk/config-runtime": fileURLToPath(
new URL("./capability-runtime-vitest-shims/config-runtime.ts", import.meta.url),
),
"openclaw/plugin-sdk/media-runtime": fileURLToPath(
new URL("./capability-runtime-vitest-shims/media-runtime.ts", import.meta.url),
),
"@openclaw/plugin-sdk/media-runtime": fileURLToPath(
new URL("./capability-runtime-vitest-shims/media-runtime.ts", import.meta.url),
),
"openclaw/plugin-sdk/provider-onboard": fileURLToPath(
new URL("../plugin-sdk/provider-onboard.ts", import.meta.url),
),
"@openclaw/plugin-sdk/provider-onboard": fileURLToPath(
new URL("../plugin-sdk/provider-onboard.ts", import.meta.url),
),
"openclaw/plugin-sdk/speech-core": fileURLToPath(
new URL("./capability-runtime-vitest-shims/speech-core.ts", import.meta.url),
),
"@openclaw/plugin-sdk/speech-core": fileURLToPath(
new URL("./capability-runtime-vitest-shims/speech-core.ts", import.meta.url),
),
};
}

View File

@@ -0,0 +1,79 @@
import { existsSync, readFileSync } from "node:fs";
import { resolve } from "node:path";
import { describe, expect, it } from "vitest";
const REPO_ROOT = resolve(import.meta.dirname, "../../..");
type TsConfigJson = {
extends?: unknown;
compilerOptions?: {
paths?: unknown;
rootDir?: unknown;
outDir?: unknown;
declaration?: unknown;
emitDeclarationOnly?: unknown;
};
include?: unknown;
exclude?: unknown;
};
type PackageJson = {
name?: unknown;
exports?: Record<string, { types?: unknown; default?: unknown }>;
devDependencies?: Record<string, string>;
};
function readJsonFile<T>(relativePath: string): T {
return JSON.parse(readFileSync(resolve(REPO_ROOT, relativePath), "utf8")) as T;
}
describe("opt-in extension package boundaries", () => {
it("keeps the opt-in extension base on real package resolution", () => {
const tsconfig = readJsonFile<TsConfigJson>("extensions/tsconfig.package-boundary.base.json");
expect(tsconfig.extends).toBe("../tsconfig.json");
expect(tsconfig.compilerOptions?.paths).toEqual({
"@openclaw/plugin-sdk/*": ["packages/plugin-sdk/dist/packages/plugin-sdk/src/*.d.ts"],
});
});
it("roots xai inside its own package and depends on the package sdk", () => {
const tsconfig = readJsonFile<TsConfigJson>("extensions/xai/tsconfig.json");
expect(tsconfig.extends).toBe("../tsconfig.package-boundary.base.json");
expect(tsconfig.compilerOptions?.rootDir).toBe(".");
expect(tsconfig.include).toEqual(["./*.ts", "./src/**/*.ts"]);
expect(tsconfig.exclude).toEqual(["./**/*.test.ts", "./dist/**", "./node_modules/**"]);
const packageJson = readJsonFile<PackageJson>("extensions/xai/package.json");
expect(packageJson.devDependencies?.["@openclaw/plugin-sdk"]).toBe("workspace:*");
});
it("keeps plugin-sdk package types generated from the package build, not a hand-maintained types bridge", () => {
const tsconfig = readJsonFile<TsConfigJson>("packages/plugin-sdk/tsconfig.json");
expect(tsconfig.extends).toBe("../../tsconfig.json");
expect(tsconfig.compilerOptions?.declaration).toBe(true);
expect(tsconfig.compilerOptions?.emitDeclarationOnly).toBe(true);
expect(tsconfig.compilerOptions?.outDir).toBe("dist");
expect(tsconfig.compilerOptions?.rootDir).toBe("../..");
expect(tsconfig.include).toEqual([
"src/**/*.ts",
"../../src/types/**/*.d.ts",
"../../packages/memory-host-sdk/src/**/*.ts",
]);
const packageJson = readJsonFile<PackageJson>("packages/plugin-sdk/package.json");
expect(packageJson.name).toBe("@openclaw/plugin-sdk");
expect(packageJson.exports?.["./core"]).toBeUndefined();
expect(packageJson.exports?.["./plugin-entry"]?.types).toBe(
"./dist/packages/plugin-sdk/src/plugin-entry.d.ts",
);
expect(packageJson.exports?.["./provider-http"]?.types).toBe(
"./dist/packages/plugin-sdk/src/provider-http.d.ts",
);
expect(packageJson.exports?.["./video-generation"]?.types).toBe(
"./dist/packages/plugin-sdk/src/video-generation.d.ts",
);
expect(existsSync(resolve(REPO_ROOT, "packages/plugin-sdk/types/plugin-entry.d.ts"))).toBe(
false,
);
});
});

View File

@@ -134,7 +134,12 @@ export function getPluginBoundaryJiti(
modulePath,
});
const aliasMap = {
...(pluginSdkAlias ? { "openclaw/plugin-sdk": pluginSdkAlias } : {}),
...(pluginSdkAlias
? {
"openclaw/plugin-sdk": pluginSdkAlias,
"@openclaw/plugin-sdk": pluginSdkAlias,
}
: {}),
...resolvePluginSdkScopedAliasMap({ modulePath }),
};
const loader = createJiti(import.meta.url, {

View File

@@ -221,10 +221,16 @@ function expectPluginSdkAliasTargets(
expect(fs.realpathSync(aliases["openclaw/plugin-sdk"] ?? "")).toBe(
fs.realpathSync(params.rootAliasPath),
);
expect(fs.realpathSync(aliases["@openclaw/plugin-sdk"] ?? "")).toBe(
fs.realpathSync(params.rootAliasPath),
);
if (params.channelRuntimePath) {
expect(fs.realpathSync(aliases["openclaw/plugin-sdk/channel-runtime"] ?? "")).toBe(
fs.realpathSync(params.channelRuntimePath),
);
expect(fs.realpathSync(aliases["@openclaw/plugin-sdk/channel-runtime"] ?? "")).toBe(
fs.realpathSync(params.channelRuntimePath),
);
}
}
@@ -715,7 +721,7 @@ describe("plugin sdk alias helpers", () => {
fs.writeFileSync(jitiBaseFile, "export {};\n", "utf-8");
fs.writeFileSync(
path.join(copiedSourceDir, "channel.runtime.ts"),
`import { resolveOutboundSendDep } from "openclaw/plugin-sdk/infra-runtime";
`import { resolveOutboundSendDep } from "@openclaw/plugin-sdk/infra-runtime";
export const syntheticRuntimeMarker = {
resolveOutboundSendDep,
@@ -745,6 +751,7 @@ export const syntheticRuntimeMarker = {
const withAlias = createJiti(jitiBaseUrl, {
...buildPluginLoaderJitiOptions({
"openclaw/plugin-sdk/infra-runtime": copiedChannelRuntimeShim,
"@openclaw/plugin-sdk/infra-runtime": copiedChannelRuntimeShim,
}),
tryNative: false,
});

View File

@@ -250,6 +250,7 @@ export function resolvePluginSdkAliasFile(params: {
const cachedPluginSdkExportedSubpaths = new Map<string, string[]>();
const cachedPluginSdkScopedAliasMaps = new Map<string, Record<string, string>>();
const PLUGIN_SDK_PACKAGE_NAMES = ["openclaw/plugin-sdk", "@openclaw/plugin-sdk"] as const;
export function listPluginSdkExportedSubpaths(
params: {
@@ -318,7 +319,9 @@ export function resolvePluginSdkScopedAliasMap(
for (const kind of orderedKinds) {
const candidate = candidateMap[kind];
if (fs.existsSync(candidate)) {
aliasMap[`openclaw/plugin-sdk/${subpath}`] = candidate;
for (const packageName of PLUGIN_SDK_PACKAGE_NAMES) {
aliasMap[`${packageName}/${subpath}`] = candidate;
}
break;
}
}
@@ -376,7 +379,12 @@ export function buildPluginLoaderAliasMap(
? { "openclaw/extension-api": normalizeJitiAliasTargetPath(extensionApiAlias) }
: {}),
...(pluginSdkAlias
? { "openclaw/plugin-sdk": normalizeJitiAliasTargetPath(pluginSdkAlias) }
? Object.fromEntries(
PLUGIN_SDK_PACKAGE_NAMES.map((packageName) => [
packageName,
normalizeJitiAliasTargetPath(pluginSdkAlias),
]),
)
: {}),
...Object.fromEntries(
Object.entries(