fix(plugin-sdk): align speech runtime packaging (#89899)

Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
xingzhou 2026-07-07 14:38:41 +08:00 committed by GitHub
parent 5537bc9c4d
commit de152b1f65
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 109 additions and 14 deletions

View file

@ -23,6 +23,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- **Packaged speech runtime:** stop treating package-backed `speech-core` as a bundled plugin sidecar, restoring TTS startup in npm installs while release checks keep true activation-bypassing facades package-complete. (#89899, #89425) Thanks @zhangguiping-xydt.
- **Codex app-server protocol:** require app-server 0.142 or newer, remove pre-0.142 wire-shape compatibility, and teach Codex to retrieve deferred native `spawn_agent` through `tool_search` so native subagent task mirroring works on search-capable models. (#101221)
- **Android hardware keyboard chat:** send with unmodified Enter on physical keyboards while preserving Shift+Enter and other modified Enter combinations for multiline input. (#101239) Thanks @3ninyt3nin-creator.
- **CJK Markdown emphasis:** render adjacent Chinese, Japanese, and Korean emphasis punctuation through the shared Markdown pipeline instead of leaking literal markers across channels. (#101230, #101120) Thanks @nicknmorty.

View file

@ -8,7 +8,6 @@ import { normalizeStringEntries, uniqueStrings } from "openclaw/plugin-sdk/strin
const QA_ALWAYS_STAGE_RUNTIME_PLUGIN_IDS = Object.freeze([
"image-generation-core",
"media-understanding-core",
"speech-core",
]);
const QA_OPENAI_PLUGIN_ID = "openai";
const QA_BUNDLED_PLUGIN_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;

View file

@ -1529,7 +1529,9 @@ describe("qa bundled plugin dir", () => {
);
await mkdir(path.join(repoRoot, "dist", "extensions", "qa-channel"), { recursive: true });
await mkdir(path.join(repoRoot, "dist", "extensions", "memory-core"), { recursive: true });
await mkdir(path.join(repoRoot, "dist", "extensions", "speech-core"), { recursive: true });
await mkdir(path.join(repoRoot, "dist", "extensions", "image-generation-core"), {
recursive: true,
});
await mkdir(path.join(repoRoot, "dist", "extensions", "unused-plugin"), { recursive: true });
await mkdir(path.join(repoRoot, "dist", "plugin-sdk"), { recursive: true });
await writeFile(
@ -1564,9 +1566,9 @@ describe("qa bundled plugin dir", () => {
});
expect((await readdir(bundledPluginsDir)).toSorted()).toEqual([
"image-generation-core",
"memory-core",
"qa-channel",
"speech-core",
]);
expect(bundledPluginsDir).toBe(
path.join(
@ -1590,7 +1592,9 @@ describe("qa bundled plugin dir", () => {
expect(qaChannel.accountId).toBe("qa");
expect((await lstat(path.join(bundledPluginsDir, "qa-channel"))).isDirectory()).toBe(true);
expect((await lstat(path.join(bundledPluginsDir, "memory-core"))).isDirectory()).toBe(true);
expect((await lstat(path.join(bundledPluginsDir, "speech-core"))).isDirectory()).toBe(true);
expect((await lstat(path.join(bundledPluginsDir, "image-generation-core"))).isDirectory()).toBe(
true,
);
const sharedChunkStat = await lstat(
path.join(
repoRoot,
@ -2064,9 +2068,9 @@ describe("qa bundled plugin dir", () => {
JSON.stringify({ openclaw: { install: { minHostVersion: ">=2026.4.8" } } }),
"utf8",
);
await mkdir(path.join(bundledRoot, "speech-core"), { recursive: true });
await mkdir(path.join(bundledRoot, "image-generation-core"), { recursive: true });
await writeFile(
path.join(bundledRoot, "speech-core", "package.json"),
path.join(bundledRoot, "image-generation-core", "package.json"),
JSON.stringify({ openclaw: { install: { minHostVersion: ">=2026.4.9" } } }),
"utf8",
);

View file

@ -25,6 +25,7 @@ import {
} from "node:path";
import { pathToFileURL } from "node:url";
import { formatErrorMessage } from "../src/infra/errors.ts";
import { ALWAYS_ALLOWED_RUNTIME_DIR_NAMES } from "../src/plugin-sdk/facade-activation-contract.ts";
import { BUNDLED_RUNTIME_SIDECAR_PATHS } from "../src/plugins/runtime-sidecar-paths.ts";
import { readBoundedResponseText } from "./lib/bounded-response.ts";
import { listBundledPluginPackArtifacts } from "./lib/bundled-plugin-build-entries.mjs";
@ -409,6 +410,7 @@ export function collectInstalledPackageErrors(params: {
}
errors.push(...collectInstalledBundledExtensionManifestErrors(params.packageRoot));
errors.push(...collectInstalledAlwaysAllowedRuntimeFacadeErrors(params.packageRoot));
errors.push(...collectInstalledContextEngineRuntimeErrors(params.packageRoot));
errors.push(...collectInstalledPluginSdkZodArtifactErrors(params.packageRoot));
errors.push(...collectInstalledPluginSdkDeclarationErrors(params.packageRoot));
@ -417,6 +419,25 @@ export function collectInstalledPackageErrors(params: {
return errors;
}
export function collectInstalledAlwaysAllowedRuntimeFacadeErrors(packageRoot: string): string[] {
const errors: string[] = [];
const activationRuntimePath = "dist/facade-activation-check.runtime.js";
if (!existsSync(join(packageRoot, activationRuntimePath))) {
errors.push(
`installed package is missing required facade activation runtime: ${activationRuntimePath}`,
);
}
for (const dirName of ALWAYS_ALLOWED_RUNTIME_DIR_NAMES) {
const relativePath = `dist/extensions/${dirName}/runtime-api.js`;
if (!existsSync(join(packageRoot, relativePath))) {
errors.push(
`installed package allows bundled runtime facade ${dirName}/runtime-api.js but is missing required runtime sidecar: ${relativePath}.`,
);
}
}
return errors;
}
function collectInstalledBundledExtensionIds(packageRoot: string): Set<string> {
const extensionsDir = join(packageRoot, "dist", "extensions");
if (!existsSync(extensionsDir)) {

View file

@ -25,13 +25,10 @@ import {
type PluginManifestRecord,
} from "../plugins/manifest-registry.js";
import { parseJsonWithJson5Fallback } from "../utils/parse-json-compat.js";
import { ALWAYS_ALLOWED_RUNTIME_DIR_NAMES } from "./facade-activation-contract.js";
import { resolveRegistryPluginModuleLocationFromRecords } from "./facade-resolution-shared.js";
const ALWAYS_ALLOWED_RUNTIME_DIR_NAMES = new Set([
"image-generation-core",
"media-understanding-core",
"speech-core",
]);
const ALWAYS_ALLOWED_RUNTIME_DIR_NAME_SET = new Set<string>(ALWAYS_ALLOWED_RUNTIME_DIR_NAMES);
const EMPTY_FACADE_BOUNDARY_CONFIG: OpenClawConfig = {};
/** Minimal manifest shape needed to decide whether a bundled facade may load. */
@ -265,7 +262,7 @@ export function resolveBundledPluginPublicSurfaceAccess(params: {
}): { allowed: boolean; pluginId?: string; reason?: string } {
if (
params.artifactBasename === "runtime-api.js" &&
ALWAYS_ALLOWED_RUNTIME_DIR_NAMES.has(params.dirName)
ALWAYS_ALLOWED_RUNTIME_DIR_NAME_SET.has(params.dirName)
) {
return {
allowed: true,

View file

@ -0,0 +1,6 @@
// Runtime facades with no activation-owned plugin manifest. Every entry must
// ship a matching dist/extensions/<id>/runtime-api.js sidecar.
export const ALWAYS_ALLOWED_RUNTIME_DIR_NAMES = [
"image-generation-core",
"media-understanding-core",
] as const;

View file

@ -547,10 +547,10 @@ describe("plugin-sdk facade runtime", () => {
});
});
it("keeps shared runtime-core facades available without plugin activation", () => {
it("keeps bundled extension runtime-core facades available without plugin activation", () => {
setRuntimeConfigSnapshot({});
for (const dirName of ["speech-core", "image-generation-core", "media-understanding-core"]) {
for (const dirName of ["image-generation-core", "media-understanding-core"]) {
expect(
resolveActivationCheckBundledPluginPublicSurfaceAccess({
dirName,
@ -566,6 +566,23 @@ describe("plugin-sdk facade runtime", () => {
}
});
it("does not treat package-backed speech-core as a bundled extension facade", () => {
setRuntimeConfigSnapshot({});
expect(
resolveActivationCheckBundledPluginPublicSurfaceAccess({
dirName: "speech-core",
artifactBasename: "runtime-api.js",
location: null,
sourceExtensionsRoot: "",
resolutionKey: "runtime-core:speech-core",
}),
).toEqual({
allowed: false,
reason: "no bundled plugin manifest found for speech-core",
});
});
it("prefers the source runtime snapshot for facade activation checks", () => {
const dir = createTempDirSync("openclaw-facade-source-snapshot-");
fs.mkdirSync(path.join(dir, "demo"), { recursive: true });

View file

@ -7,6 +7,7 @@ import { describe, expect, it, vi } from "vitest";
import {
buildPublishedInstallCommandArgs,
buildPublishedInstallScenarios,
collectInstalledAlwaysAllowedRuntimeFacadeErrors,
collectInstalledBundledExtensionManifestErrors,
collectInstalledBundledRuntimeSidecarPaths,
collectInstalledContextEngineRuntimeErrors,
@ -447,6 +448,43 @@ describe("collectInstalledPackageErrors", () => {
});
});
describe("collectInstalledAlwaysAllowedRuntimeFacadeErrors", () => {
function withInstalledPackageRoot(run: (packageRoot: string) => void): void {
const packageRoot = mkdtempSync(join(tmpdir(), "openclaw-postpublish-facade-runtime-"));
try {
run(packageRoot);
} finally {
rmSync(packageRoot, { recursive: true, force: true });
}
}
function writeInstalledFile(packageRoot: string, relativePath: string): void {
const filePath = join(packageRoot, relativePath);
mkdirSync(dirname(filePath), { recursive: true });
writeFileSync(filePath, "export {};\n", "utf8");
}
it("reports the activation runtime and every missing allowlisted sidecar", () => {
withInstalledPackageRoot((packageRoot) => {
expect(collectInstalledAlwaysAllowedRuntimeFacadeErrors(packageRoot)).toEqual([
"installed package is missing required facade activation runtime: dist/facade-activation-check.runtime.js",
"installed package allows bundled runtime facade image-generation-core/runtime-api.js but is missing required runtime sidecar: dist/extensions/image-generation-core/runtime-api.js.",
"installed package allows bundled runtime facade media-understanding-core/runtime-api.js but is missing required runtime sidecar: dist/extensions/media-understanding-core/runtime-api.js.",
]);
});
});
it("accepts a package with the activation runtime and allowlisted sidecars", () => {
withInstalledPackageRoot((packageRoot) => {
writeInstalledFile(packageRoot, "dist/facade-activation-check.runtime.js");
writeInstalledFile(packageRoot, "dist/extensions/image-generation-core/runtime-api.js");
writeInstalledFile(packageRoot, "dist/extensions/media-understanding-core/runtime-api.js");
expect(collectInstalledAlwaysAllowedRuntimeFacadeErrors(packageRoot)).toStrictEqual([]);
});
});
});
describe("collectInstalledContextEngineRuntimeErrors", () => {
function makeInstalledPackageRoot(): string {
return mkdtempSync(join(tmpdir(), "openclaw-postpublish-context-engine-"));

View file

@ -729,6 +729,18 @@ describe("collectMissingPackPaths", () => {
const packageRoot = join(root, "openclaw");
const distDir = join(packageRoot, "dist");
mkdirSync(distDir, { recursive: true });
for (const relativePath of [
"facade-activation-check.runtime.js",
"extensions/image-generation-core/runtime-api.js",
"extensions/media-understanding-core/runtime-api.js",
]) {
const filePath = join(distDir, relativePath);
mkdirSync(dirname(filePath), { recursive: true });
writeFileSync(filePath, "export {};\n");
}
for (const pluginId of ["image-generation-core", "media-understanding-core"]) {
writeFileSync(join(distDir, "extensions", pluginId, "package.json"), "{}\n");
}
writeFileSync(
join(packageRoot, "package.json"),
`${JSON.stringify({ name: "openclaw", version: "2026.5.14-beta.3", dependencies: {} })}\n`,