diff --git a/CHANGELOG.md b/CHANGELOG.md index 43118dee25d..87bdb001a2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- Plugin SDK/testing: lazy-load TypeScript from the plugin test-contract runtime and add release checks for critical SDK contract entrypoint imports and bundle size, so published packages fail preflight before shipping ESM-incompatible or oversized contract helpers. Thanks @vincentkoc. - CLI/browser: preserve parent flags while lazy-loading browser subcommands, so `openclaw browser --json open` and `openclaw browser --json tabs` keep machine-readable output after reparsing. Fixes #74574. Thanks @devintegeritsm. - Plugins/runtime-deps: add `openclaw plugins deps` inspection and repair with script-free package-manager defaults shared across plugin installers, so operators can repair missing bundled runtime deps without corrupting JSON output or blocking unrelated conflict-free deps. Thanks @vincentkoc. - Agents/output: strip internal `[tool calls omitted]` replay placeholders from user-facing replies while preserving visible reply whitespace. Fixes #74573. Thanks @blaspat. diff --git a/scripts/release-check.ts b/scripts/release-check.ts index 764bdf1e978..98faae157c7 100755 --- a/scripts/release-check.ts +++ b/scripts/release-check.ts @@ -3,6 +3,7 @@ import { execFileSync, execSync } from "node:child_process"; import { existsSync, + lstatSync, mkdtempSync, mkdirSync, realpathSync, @@ -116,6 +117,15 @@ const appcastPath = resolve("appcast.xml"); const laneBuildMin = 1_000_000_000; const laneFloorAdoptionDateKey = 20260227; const SAFE_UNIX_SMOKE_PATH = "/usr/bin:/bin"; +export const MAX_CRITICAL_PLUGIN_SDK_ENTRYPOINT_BYTES = 2 * 1024 * 1024; +export const CRITICAL_PLUGIN_SDK_SIZE_CHECK_SPECIFIERS = [ + "openclaw/plugin-sdk/agent-runtime-test-contracts", + "openclaw/plugin-sdk/plugin-test-contracts", + "openclaw/plugin-sdk/provider-test-contracts", +] as const; +export const CRITICAL_PLUGIN_SDK_IMPORT_SMOKE_SPECIFIERS = [ + "openclaw/plugin-sdk/plugin-test-contracts", +] as const; export const PACKED_CLI_SMOKE_COMMANDS = [ ["--help"], ["onboard", "--help"], @@ -843,6 +853,44 @@ async function checkPluginSdkExports() { } } +export function collectCriticalPluginSdkEntrypointSizeErrors(rootDir = process.cwd()): string[] { + const errors: string[] = []; + for (const specifier of CRITICAL_PLUGIN_SDK_SIZE_CHECK_SPECIFIERS) { + const subpath = specifier.slice("openclaw/plugin-sdk/".length); + const relativePath = `dist/plugin-sdk/${subpath}.js`; + const filePath = resolve(rootDir, relativePath); + if (!existsSync(filePath)) { + errors.push(`${relativePath} is missing.`); + continue; + } + const stat = lstatSync(filePath); + if (!stat.isFile()) { + errors.push(`${relativePath} is not a file.`); + continue; + } + if (stat.size > MAX_CRITICAL_PLUGIN_SDK_ENTRYPOINT_BYTES) { + errors.push( + `${relativePath} is ${stat.size} bytes, exceeding ${MAX_CRITICAL_PLUGIN_SDK_ENTRYPOINT_BYTES} bytes. Keep public SDK test-contract entrypoints lazy and avoid bundling compiler/runtime internals.`, + ); + } + } + return errors; +} + +function runCriticalPluginSdkEntrypointImportSmoke() { + const script = [ + `const specifiers = ${JSON.stringify(CRITICAL_PLUGIN_SDK_IMPORT_SMOKE_SPECIFIERS)};`, + `const importModule = new Function("specifier", "return imp" + "ort(specifier)");`, + "for (const specifier of specifiers) {", + " await importModule(specifier);", + "}", + ].join("\n"); + execFileSync(process.execPath, ["--input-type=module", "--eval", script], { + cwd: process.cwd(), + stdio: "inherit", + }); +} + async function main() { checkAppcastSparkleVersions(); checkCliBootstrapExternalImports({ @@ -851,6 +899,15 @@ async function main() { }, }); await checkPluginSdkExports(); + const criticalPluginSdkEntrypointErrors = collectCriticalPluginSdkEntrypointSizeErrors(); + if (criticalPluginSdkEntrypointErrors.length > 0) { + console.error("release-check: critical plugin-sdk entrypoint validation failed:"); + for (const error of criticalPluginSdkEntrypointErrors) { + console.error(` - ${error}`); + } + process.exit(1); + } + runCriticalPluginSdkEntrypointImportSmoke(); checkBundledExtensionMetadata(); await writePackageDistInventory(process.cwd()); diff --git a/src/plugin-sdk/test-helpers/jiti-runtime-api.ts b/src/plugin-sdk/test-helpers/jiti-runtime-api.ts index 2bc58505a67..60d2ca97002 100644 --- a/src/plugin-sdk/test-helpers/jiti-runtime-api.ts +++ b/src/plugin-sdk/test-helpers/jiti-runtime-api.ts @@ -1,7 +1,13 @@ import { execFileSync } from "node:child_process"; import { existsSync, readFileSync } from "node:fs"; +import { createRequire } from "node:module"; import path from "node:path"; -import ts from "typescript"; + +const nodeRequire = createRequire(import.meta.url); + +function loadTypeScript(): typeof import("typescript") { + return nodeRequire("typescript") as typeof import("typescript"); +} const JITI_EXTENSIONS = [ ".ts", @@ -79,6 +85,7 @@ function resolveLocalModulePath(filePath: string, specifier: string): string | n } function collectSourceModuleRefs(filePath: string): SourceModuleRef[] { + const ts = loadTypeScript(); const sourceText = readFileSync(filePath, "utf8"); const sourceFile = ts.createSourceFile(filePath, sourceText, ts.ScriptTarget.Latest, true); const refs: SourceModuleRef[] = []; diff --git a/test/release-check.test.ts b/test/release-check.test.ts index d951bdfd018..f330bdeb430 100644 --- a/test/release-check.test.ts +++ b/test/release-check.test.ts @@ -11,6 +11,7 @@ import { collectAppcastSparkleVersionErrors, collectBundledExtensionManifestErrors, collectBundledPluginRootRuntimeMirrorErrors, + collectCriticalPluginSdkEntrypointSizeErrors, collectDeclaredRootRuntimeDependencyMetadataErrors, collectForbiddenPackContentPaths, collectInstalledBundledPluginRuntimeDepErrors, @@ -21,6 +22,7 @@ import { collectPackUnpackedSizeErrors, createPackedCliSmokeEnv, createPackedBundledPluginPostinstallEnv, + MAX_CRITICAL_PLUGIN_SDK_ENTRYPOINT_BYTES, PACKED_CLI_SMOKE_COMMANDS, packageNameFromSpecifier, resolveMissingPackBuildHint, @@ -729,6 +731,30 @@ describe("collectPackUnpackedSizeErrors", () => { }); }); +describe("collectCriticalPluginSdkEntrypointSizeErrors", () => { + it("flags oversized plugin SDK test-contract entrypoints before publish", () => { + const root = mkdtempSync(join(tmpdir(), "release-check-critical-sdk-")); + try { + const pluginSdkDir = join(root, "dist", "plugin-sdk"); + mkdirSync(pluginSdkDir, { recursive: true }); + writeFileSync(join(pluginSdkDir, "agent-runtime-test-contracts.js"), "export {};\n"); + writeFileSync(join(pluginSdkDir, "provider-test-contracts.js"), "export {};\n"); + writeFileSync( + join(pluginSdkDir, "plugin-test-contracts.js"), + "x".repeat(MAX_CRITICAL_PLUGIN_SDK_ENTRYPOINT_BYTES + 1), + ); + + expect(collectCriticalPluginSdkEntrypointSizeErrors(root)).toEqual([ + `dist/plugin-sdk/plugin-test-contracts.js is ${ + MAX_CRITICAL_PLUGIN_SDK_ENTRYPOINT_BYTES + 1 + } bytes, exceeding ${MAX_CRITICAL_PLUGIN_SDK_ENTRYPOINT_BYTES} bytes. Keep public SDK test-contract entrypoints lazy and avoid bundling compiler/runtime internals.`, + ]); + } finally { + rmSync(root, { recursive: true, force: true }); + } + }); +}); + describe("createPackedBundledPluginPostinstallEnv", () => { it("keeps packed postinstall on the lazy bundled dependency path", () => { expect(createPackedBundledPluginPostinstallEnv({ PATH: "/usr/bin" })).toEqual({