Skip to content

Commit de152b1

Browse files
fix(plugin-sdk): align speech runtime packaging (#89899)
Co-authored-by: Peter Steinberger <[email protected]>
1 parent 5537bc9 commit de152b1

9 files changed

Lines changed: 109 additions & 14 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ Docs: https://docs.openclaw.ai
2323

2424
### Fixes
2525

26+
- **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.
2627
- **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)
2728
- **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.
2829
- **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.

extensions/qa-lab/src/bundled-plugin-staging.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import { normalizeStringEntries, uniqueStrings } from "openclaw/plugin-sdk/strin
88
const QA_ALWAYS_STAGE_RUNTIME_PLUGIN_IDS = Object.freeze([
99
"image-generation-core",
1010
"media-understanding-core",
11-
"speech-core",
1211
]);
1312
const QA_OPENAI_PLUGIN_ID = "openai";
1413
const QA_BUNDLED_PLUGIN_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;

extensions/qa-lab/src/gateway-child.test.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1529,7 +1529,9 @@ describe("qa bundled plugin dir", () => {
15291529
);
15301530
await mkdir(path.join(repoRoot, "dist", "extensions", "qa-channel"), { recursive: true });
15311531
await mkdir(path.join(repoRoot, "dist", "extensions", "memory-core"), { recursive: true });
1532-
await mkdir(path.join(repoRoot, "dist", "extensions", "speech-core"), { recursive: true });
1532+
await mkdir(path.join(repoRoot, "dist", "extensions", "image-generation-core"), {
1533+
recursive: true,
1534+
});
15331535
await mkdir(path.join(repoRoot, "dist", "extensions", "unused-plugin"), { recursive: true });
15341536
await mkdir(path.join(repoRoot, "dist", "plugin-sdk"), { recursive: true });
15351537
await writeFile(
@@ -1564,9 +1566,9 @@ describe("qa bundled plugin dir", () => {
15641566
});
15651567

15661568
expect((await readdir(bundledPluginsDir)).toSorted()).toEqual([
1569+
"image-generation-core",
15671570
"memory-core",
15681571
"qa-channel",
1569-
"speech-core",
15701572
]);
15711573
expect(bundledPluginsDir).toBe(
15721574
path.join(
@@ -1590,7 +1592,9 @@ describe("qa bundled plugin dir", () => {
15901592
expect(qaChannel.accountId).toBe("qa");
15911593
expect((await lstat(path.join(bundledPluginsDir, "qa-channel"))).isDirectory()).toBe(true);
15921594
expect((await lstat(path.join(bundledPluginsDir, "memory-core"))).isDirectory()).toBe(true);
1593-
expect((await lstat(path.join(bundledPluginsDir, "speech-core"))).isDirectory()).toBe(true);
1595+
expect((await lstat(path.join(bundledPluginsDir, "image-generation-core"))).isDirectory()).toBe(
1596+
true,
1597+
);
15941598
const sharedChunkStat = await lstat(
15951599
path.join(
15961600
repoRoot,
@@ -2064,9 +2068,9 @@ describe("qa bundled plugin dir", () => {
20642068
JSON.stringify({ openclaw: { install: { minHostVersion: ">=2026.4.8" } } }),
20652069
"utf8",
20662070
);
2067-
await mkdir(path.join(bundledRoot, "speech-core"), { recursive: true });
2071+
await mkdir(path.join(bundledRoot, "image-generation-core"), { recursive: true });
20682072
await writeFile(
2069-
path.join(bundledRoot, "speech-core", "package.json"),
2073+
path.join(bundledRoot, "image-generation-core", "package.json"),
20702074
JSON.stringify({ openclaw: { install: { minHostVersion: ">=2026.4.9" } } }),
20712075
"utf8",
20722076
);

scripts/openclaw-npm-postpublish-verify.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
} from "node:path";
2626
import { pathToFileURL } from "node:url";
2727
import { formatErrorMessage } from "../src/infra/errors.ts";
28+
import { ALWAYS_ALLOWED_RUNTIME_DIR_NAMES } from "../src/plugin-sdk/facade-activation-contract.ts";
2829
import { BUNDLED_RUNTIME_SIDECAR_PATHS } from "../src/plugins/runtime-sidecar-paths.ts";
2930
import { readBoundedResponseText } from "./lib/bounded-response.ts";
3031
import { listBundledPluginPackArtifacts } from "./lib/bundled-plugin-build-entries.mjs";
@@ -409,6 +410,7 @@ export function collectInstalledPackageErrors(params: {
409410
}
410411

411412
errors.push(...collectInstalledBundledExtensionManifestErrors(params.packageRoot));
413+
errors.push(...collectInstalledAlwaysAllowedRuntimeFacadeErrors(params.packageRoot));
412414
errors.push(...collectInstalledContextEngineRuntimeErrors(params.packageRoot));
413415
errors.push(...collectInstalledPluginSdkZodArtifactErrors(params.packageRoot));
414416
errors.push(...collectInstalledPluginSdkDeclarationErrors(params.packageRoot));
@@ -417,6 +419,25 @@ export function collectInstalledPackageErrors(params: {
417419
return errors;
418420
}
419421

422+
export function collectInstalledAlwaysAllowedRuntimeFacadeErrors(packageRoot: string): string[] {
423+
const errors: string[] = [];
424+
const activationRuntimePath = "dist/facade-activation-check.runtime.js";
425+
if (!existsSync(join(packageRoot, activationRuntimePath))) {
426+
errors.push(
427+
`installed package is missing required facade activation runtime: ${activationRuntimePath}`,
428+
);
429+
}
430+
for (const dirName of ALWAYS_ALLOWED_RUNTIME_DIR_NAMES) {
431+
const relativePath = `dist/extensions/${dirName}/runtime-api.js`;
432+
if (!existsSync(join(packageRoot, relativePath))) {
433+
errors.push(
434+
`installed package allows bundled runtime facade ${dirName}/runtime-api.js but is missing required runtime sidecar: ${relativePath}.`,
435+
);
436+
}
437+
}
438+
return errors;
439+
}
440+
420441
function collectInstalledBundledExtensionIds(packageRoot: string): Set<string> {
421442
const extensionsDir = join(packageRoot, "dist", "extensions");
422443
if (!existsSync(extensionsDir)) {

src/plugin-sdk/facade-activation-check.runtime.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,10 @@ import {
2525
type PluginManifestRecord,
2626
} from "../plugins/manifest-registry.js";
2727
import { parseJsonWithJson5Fallback } from "../utils/parse-json-compat.js";
28+
import { ALWAYS_ALLOWED_RUNTIME_DIR_NAMES } from "./facade-activation-contract.js";
2829
import { resolveRegistryPluginModuleLocationFromRecords } from "./facade-resolution-shared.js";
2930

30-
const ALWAYS_ALLOWED_RUNTIME_DIR_NAMES = new Set([
31-
"image-generation-core",
32-
"media-understanding-core",
33-
"speech-core",
34-
]);
31+
const ALWAYS_ALLOWED_RUNTIME_DIR_NAME_SET = new Set<string>(ALWAYS_ALLOWED_RUNTIME_DIR_NAMES);
3532
const EMPTY_FACADE_BOUNDARY_CONFIG: OpenClawConfig = {};
3633

3734
/** Minimal manifest shape needed to decide whether a bundled facade may load. */
@@ -265,7 +262,7 @@ export function resolveBundledPluginPublicSurfaceAccess(params: {
265262
}): { allowed: boolean; pluginId?: string; reason?: string } {
266263
if (
267264
params.artifactBasename === "runtime-api.js" &&
268-
ALWAYS_ALLOWED_RUNTIME_DIR_NAMES.has(params.dirName)
265+
ALWAYS_ALLOWED_RUNTIME_DIR_NAME_SET.has(params.dirName)
269266
) {
270267
return {
271268
allowed: true,
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
// Runtime facades with no activation-owned plugin manifest. Every entry must
2+
// ship a matching dist/extensions/<id>/runtime-api.js sidecar.
3+
export const ALWAYS_ALLOWED_RUNTIME_DIR_NAMES = [
4+
"image-generation-core",
5+
"media-understanding-core",
6+
] as const;

src/plugin-sdk/facade-runtime.test.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -547,10 +547,10 @@ describe("plugin-sdk facade runtime", () => {
547547
});
548548
});
549549

550-
it("keeps shared runtime-core facades available without plugin activation", () => {
550+
it("keeps bundled extension runtime-core facades available without plugin activation", () => {
551551
setRuntimeConfigSnapshot({});
552552

553-
for (const dirName of ["speech-core", "image-generation-core", "media-understanding-core"]) {
553+
for (const dirName of ["image-generation-core", "media-understanding-core"]) {
554554
expect(
555555
resolveActivationCheckBundledPluginPublicSurfaceAccess({
556556
dirName,
@@ -566,6 +566,23 @@ describe("plugin-sdk facade runtime", () => {
566566
}
567567
});
568568

569+
it("does not treat package-backed speech-core as a bundled extension facade", () => {
570+
setRuntimeConfigSnapshot({});
571+
572+
expect(
573+
resolveActivationCheckBundledPluginPublicSurfaceAccess({
574+
dirName: "speech-core",
575+
artifactBasename: "runtime-api.js",
576+
location: null,
577+
sourceExtensionsRoot: "",
578+
resolutionKey: "runtime-core:speech-core",
579+
}),
580+
).toEqual({
581+
allowed: false,
582+
reason: "no bundled plugin manifest found for speech-core",
583+
});
584+
});
585+
569586
it("prefers the source runtime snapshot for facade activation checks", () => {
570587
const dir = createTempDirSync("openclaw-facade-source-snapshot-");
571588
fs.mkdirSync(path.join(dir, "demo"), { recursive: true });

test/openclaw-npm-postpublish-verify.test.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { describe, expect, it, vi } from "vitest";
77
import {
88
buildPublishedInstallCommandArgs,
99
buildPublishedInstallScenarios,
10+
collectInstalledAlwaysAllowedRuntimeFacadeErrors,
1011
collectInstalledBundledExtensionManifestErrors,
1112
collectInstalledBundledRuntimeSidecarPaths,
1213
collectInstalledContextEngineRuntimeErrors,
@@ -447,6 +448,43 @@ describe("collectInstalledPackageErrors", () => {
447448
});
448449
});
449450

451+
describe("collectInstalledAlwaysAllowedRuntimeFacadeErrors", () => {
452+
function withInstalledPackageRoot(run: (packageRoot: string) => void): void {
453+
const packageRoot = mkdtempSync(join(tmpdir(), "openclaw-postpublish-facade-runtime-"));
454+
try {
455+
run(packageRoot);
456+
} finally {
457+
rmSync(packageRoot, { recursive: true, force: true });
458+
}
459+
}
460+
461+
function writeInstalledFile(packageRoot: string, relativePath: string): void {
462+
const filePath = join(packageRoot, relativePath);
463+
mkdirSync(dirname(filePath), { recursive: true });
464+
writeFileSync(filePath, "export {};\n", "utf8");
465+
}
466+
467+
it("reports the activation runtime and every missing allowlisted sidecar", () => {
468+
withInstalledPackageRoot((packageRoot) => {
469+
expect(collectInstalledAlwaysAllowedRuntimeFacadeErrors(packageRoot)).toEqual([
470+
"installed package is missing required facade activation runtime: dist/facade-activation-check.runtime.js",
471+
"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.",
472+
"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.",
473+
]);
474+
});
475+
});
476+
477+
it("accepts a package with the activation runtime and allowlisted sidecars", () => {
478+
withInstalledPackageRoot((packageRoot) => {
479+
writeInstalledFile(packageRoot, "dist/facade-activation-check.runtime.js");
480+
writeInstalledFile(packageRoot, "dist/extensions/image-generation-core/runtime-api.js");
481+
writeInstalledFile(packageRoot, "dist/extensions/media-understanding-core/runtime-api.js");
482+
483+
expect(collectInstalledAlwaysAllowedRuntimeFacadeErrors(packageRoot)).toStrictEqual([]);
484+
});
485+
});
486+
});
487+
450488
describe("collectInstalledContextEngineRuntimeErrors", () => {
451489
function makeInstalledPackageRoot(): string {
452490
return mkdtempSync(join(tmpdir(), "openclaw-postpublish-context-engine-"));

test/release-check.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -729,6 +729,18 @@ describe("collectMissingPackPaths", () => {
729729
const packageRoot = join(root, "openclaw");
730730
const distDir = join(packageRoot, "dist");
731731
mkdirSync(distDir, { recursive: true });
732+
for (const relativePath of [
733+
"facade-activation-check.runtime.js",
734+
"extensions/image-generation-core/runtime-api.js",
735+
"extensions/media-understanding-core/runtime-api.js",
736+
]) {
737+
const filePath = join(distDir, relativePath);
738+
mkdirSync(dirname(filePath), { recursive: true });
739+
writeFileSync(filePath, "export {};\n");
740+
}
741+
for (const pluginId of ["image-generation-core", "media-understanding-core"]) {
742+
writeFileSync(join(distDir, "extensions", pluginId, "package.json"), "{}\n");
743+
}
732744
writeFileSync(
733745
join(packageRoot, "package.json"),
734746
`${JSON.stringify({ name: "openclaw", version: "2026.5.14-beta.3", dependencies: {} })}\n`,

0 commit comments

Comments
 (0)