diff --git a/scripts/lib/plugin-npm-runtime-build.d.mts b/scripts/lib/plugin-npm-runtime-build.d.mts index 2df83275f7d9..8d4aab5b894a 100644 --- a/scripts/lib/plugin-npm-runtime-build.d.mts +++ b/scripts/lib/plugin-npm-runtime-build.d.mts @@ -33,7 +33,7 @@ export type PluginNpmRuntimeBuildPlan = { }; /** Resolve the package-local runtime build plan for one publishable plugin package. */ export function resolvePluginNpmRuntimeBuildPlan(params: unknown): PluginNpmRuntimeBuildPlan | null; -/** List built host SDK imports that are absent from the OpenClaw package exports. */ +/** List built host SDK imports unavailable through package exports or owner-restricted aliases. */ export function listMissingPluginNpmRuntimeHostExports(plan: PluginNpmRuntimeBuildPlan): string[]; /** Build package-local runtime files and static assets for one plugin package. */ export function buildPluginNpmRuntime(params: unknown): Promise< diff --git a/scripts/lib/plugin-npm-runtime-build.mjs b/scripts/lib/plugin-npm-runtime-build.mjs index 0381cc17c062..0fada07406ff 100644 --- a/scripts/lib/plugin-npm-runtime-build.mjs +++ b/scripts/lib/plugin-npm-runtime-build.mjs @@ -93,6 +93,10 @@ function createNeverBundleDependencyMatcher(packageJson) { const HOST_PLUGIN_SDK_IMPORT_RE = /(?:\bfrom\s+|\bimport\s*(?:\(\s*)?|\b(?:require|_+require\d*)\(\s*)["'](openclaw\/plugin-sdk\/[^"']+)["']/gu; +const OWNER_RESTRICTED_HOST_IMPORTS_BY_PACKAGE_NAME = new Map([ + ["@openclaw/codex", new Set(["openclaw/plugin-sdk/agent-harness-tool-authority-runtime"])], + ["@openclaw/copilot", new Set(["openclaw/plugin-sdk/agent-harness-tool-authority-runtime"])], +]); function listRuntimeJavaScriptFiles(rootDir) { if (!fs.existsSync(rootDir)) { @@ -111,8 +115,8 @@ function listRuntimeJavaScriptFiles(rootDir) { } /** - * List host SDK imports emitted by a built plugin runtime but absent from package exports. - * @param {{ repoRoot: string; outDir: string }} plan + * List host SDK imports emitted by a built plugin runtime but unavailable to its package owner. + * @param {{ repoRoot: string; outDir: string; packageJson?: { name?: unknown } }} plan */ export function listMissingPluginNpmRuntimeHostExports(plan) { const hostImports = new Set(); @@ -131,8 +135,16 @@ export function listMissingPluginNpmRuntimeHostExports(plan) { const hostPackageJson = readJsonFile(path.join(plan.repoRoot, "package.json")); const hostExports = new Set(Object.keys(hostPackageJson.exports ?? {})); + const packageName = + typeof plan.packageJson?.name === "string" ? plan.packageJson.name.trim() : ""; + const ownerRestrictedHostImports = + OWNER_RESTRICTED_HOST_IMPORTS_BY_PACKAGE_NAME.get(packageName) ?? new Set(); return [...hostImports] - .filter((specifier) => !hostExports.has(specifier.replace(/^openclaw/u, "."))) + .filter( + (specifier) => + !hostExports.has(specifier.replace(/^openclaw/u, ".")) && + !ownerRestrictedHostImports.has(specifier), + ) .toSorted((left, right) => left.localeCompare(right)); } diff --git a/test/plugin-npm-runtime-build.test.ts b/test/plugin-npm-runtime-build.test.ts index 29dba45f5462..05199789b970 100644 --- a/test/plugin-npm-runtime-build.test.ts +++ b/test/plugin-npm-runtime-build.test.ts @@ -240,16 +240,19 @@ describe("plugin npm runtime build planning", () => { expect(plan.runtimeBuildOutputs).toContain("./dist/setup-api.js"); }); - it("keeps published Codex runtime imports resolvable from the host package", async () => { - const result = await buildPluginNpmRuntime({ - repoRoot, - packageDir: "extensions/codex", - logLevel: "silent", - }); - const plan = expectPluginNpmRuntimeBuildPlan(result); + it.each(["codex", "copilot"])( + "keeps published %s runtime imports resolvable from the host package", + async (pluginId) => { + const result = await buildPluginNpmRuntime({ + repoRoot, + packageDir: `extensions/${pluginId}`, + logLevel: "silent", + }); + const plan = expectPluginNpmRuntimeBuildPlan(result); - expect(listMissingPluginNpmRuntimeHostExports(plan)).toEqual([]); - }); + expect(listMissingPluginNpmRuntimeHostExports(plan)).toEqual([]); + }, + ); it("detects unresolved side-effect host imports in built plugin runtimes", () => { const outDir = tempDirs.make("openclaw-plugin-runtime-host-import-"); @@ -275,6 +278,45 @@ describe("plugin npm runtime build planning", () => { ]); }); + it("allows private harness authority imports only for their official package owners", () => { + const outDir = tempDirs.make("openclaw-plugin-runtime-private-owner-import-"); + writeFileSync( + path.join(outDir, "index.js"), + [ + 'import "openclaw/plugin-sdk/agent-harness-tool-authority-runtime";', + 'import "openclaw/plugin-sdk/not-owner-restricted";', + "", + ].join("\n"), + ); + const plan = expectPluginNpmRuntimeBuildPlan( + resolvePluginNpmRuntimeBuildPlan({ + repoRoot, + packageDir: path.join(repoRoot, "extensions", "codex"), + }), + ); + + expect(listMissingPluginNpmRuntimeHostExports({ ...plan, outDir })).toEqual([ + "openclaw/plugin-sdk/not-owner-restricted", + ]); + expect( + listMissingPluginNpmRuntimeHostExports({ + ...plan, + outDir, + packageJson: { ...plan.packageJson, name: "@openclaw/copilot" }, + }), + ).toEqual(["openclaw/plugin-sdk/not-owner-restricted"]); + expect( + listMissingPluginNpmRuntimeHostExports({ + ...plan, + outDir, + packageJson: { ...plan.packageJson, name: "@openclaw/demo" }, + }), + ).toEqual([ + "openclaw/plugin-sdk/agent-harness-tool-authority-runtime", + "openclaw/plugin-sdk/not-owner-restricted", + ]); + }); + it("does not require host metadata when the runtime has no host imports", () => { const syntheticRepoRoot = tempDirs.make("openclaw-plugin-runtime-synthetic-repo-"); const outDir = tempDirs.make("openclaw-plugin-runtime-no-host-import-");