mirror of https://github.com/openclaw/openclaw.git
fix(plugins): guard runtime facade activation (#59412)
* fix(plugins): guard runtime facade activation * refactor(plugin-sdk): localize facade load policy * fix(plugin-sdk): narrow facade activation guards * fix(browser): keep cleanup helpers outside activation guard * style(browser): apply formatter follow-ups * chore(changelog): note plugin activation guard regressions * fix(discord): keep cleanup thread unbinds outside activation guard * fix(browser): fallback when trash exits non-zero
This commit is contained in:
parent
ed6012eb5b
commit
52a018680d
|
|
@ -16,6 +16,7 @@ Docs: https://docs.openclaw.ai
|
|||
- Slack/mrkdwn formatting: add built-in Slack mrkdwn guidance in inbound context so Slack replies stop falling back to generic Markdown patterns that render poorly in Slack. (#59100) Thanks @jadewon.
|
||||
- Gateway/exec loopback: restore legacy-role fallback for empty paired-device token maps and allow silent local role upgrades so local exec and node clients stop failing with pairing-required errors after `2026.3.31`. (#59092) Thanks @openperf.
|
||||
- WhatsApp/media: add HTML, XML, and CSS to the MIME map and fall back gracefully for unknown media types instead of dropping the attachment. (#51562) Thanks @bobbyt74.
|
||||
- Plugins/runtime: keep LINE reply directives and browser-backed cleanup/reset flows working even when those plugins are disabled while tightening bundled plugin activation guards. (#59412) Thanks @vincentkoc.
|
||||
- WhatsApp/presence: send `unavailable` presence on connect in self-chat mode so personal-phone users stop losing all push notifications while the gateway is running. (#59410) Thanks @mcaxtr.
|
||||
- Providers/OpenAI-compatible routing: centralize native-vs-proxy request policy so hidden attribution and related OpenAI-family defaults only apply on verified native endpoints across stream, websocket, and shared audio HTTP paths. (#59433) Thanks @vincentkoc.
|
||||
- Exec approvals/doctor: report host policy sources from the real approvals file path and ignore malformed host override values when attributing effective policy conflicts. (#59367) Thanks @gumadeiras.
|
||||
|
|
|
|||
|
|
@ -49,6 +49,8 @@ export const GENERATED_PLUGIN_SDK_FACADES = [
|
|||
{
|
||||
subpath: "discord-runtime-surface",
|
||||
source: pluginSource("discord", "runtime-api.js"),
|
||||
// Runtime entrypoints should be blocked until the owning plugin is active.
|
||||
loadPolicy: "activated",
|
||||
exports: [
|
||||
"addRoleDiscord",
|
||||
"auditDiscordChannelPermissions",
|
||||
|
|
@ -159,6 +161,10 @@ export const GENERATED_PLUGIN_SDK_FACADES = [
|
|||
{
|
||||
subpath: "discord-thread-bindings",
|
||||
source: pluginSource("discord", "runtime-api.js"),
|
||||
loadPolicy: "activated",
|
||||
directExports: {
|
||||
unbindThreadBindingsBySessionKey: "./discord-maintenance.js",
|
||||
},
|
||||
exports: [
|
||||
"autoBindSpawnedDiscordSubagent",
|
||||
"createThreadBindingManager",
|
||||
|
|
@ -199,6 +205,7 @@ export const GENERATED_PLUGIN_SDK_FACADES = [
|
|||
{
|
||||
subpath: "browser",
|
||||
source: pluginSource("browser", "runtime-api.js"),
|
||||
loadPolicy: "activated",
|
||||
exports: [
|
||||
"browserHandlers",
|
||||
"createBrowserPluginService",
|
||||
|
|
@ -210,12 +217,15 @@ export const GENERATED_PLUGIN_SDK_FACADES = [
|
|||
{
|
||||
subpath: "browser-runtime",
|
||||
source: pluginSource("browser", "runtime-api.js"),
|
||||
loadPolicy: "activated",
|
||||
directExports: {
|
||||
DEFAULT_AI_SNAPSHOT_MAX_CHARS: "./browser-config.js",
|
||||
DEFAULT_BROWSER_EVALUATE_ENABLED: "./browser-config.js",
|
||||
DEFAULT_OPENCLAW_BROWSER_COLOR: "./browser-config.js",
|
||||
DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME: "./browser-config.js",
|
||||
DEFAULT_UPLOAD_DIR: "./browser-config.js",
|
||||
closeTrackedBrowserTabsForSessions: "./browser-maintenance.js",
|
||||
movePathToTrash: "./browser-maintenance.js",
|
||||
redactCdpUrl: "./browser-config.js",
|
||||
resolveBrowserConfig: "./browser-config.js",
|
||||
resolveBrowserControlAuth: "./browser-config.js",
|
||||
|
|
@ -441,6 +451,7 @@ export const GENERATED_PLUGIN_SDK_FACADES = [
|
|||
{
|
||||
subpath: "image-generation-runtime",
|
||||
source: pluginSource("image-generation-core", "runtime-api.js"),
|
||||
loadPolicy: "activated",
|
||||
exports: [
|
||||
"generateImage",
|
||||
"listRuntimeImageGenerationProviders",
|
||||
|
|
@ -487,6 +498,7 @@ export const GENERATED_PLUGIN_SDK_FACADES = [
|
|||
{
|
||||
subpath: "media-understanding-runtime",
|
||||
source: pluginSource("media-understanding-core", "runtime-api.js"),
|
||||
loadPolicy: "activated",
|
||||
exports: [
|
||||
"describeImageFile",
|
||||
"describeImageFileWithModel",
|
||||
|
|
@ -501,6 +513,7 @@ export const GENERATED_PLUGIN_SDK_FACADES = [
|
|||
{
|
||||
subpath: "memory-core-engine-runtime",
|
||||
source: pluginSource("memory-core", "runtime-api.js"),
|
||||
loadPolicy: "activated",
|
||||
exports: [
|
||||
"BuiltinMemoryEmbeddingProviderDoctorMetadata",
|
||||
"getBuiltinMemoryEmbeddingProviderDoctorMetadata",
|
||||
|
|
@ -530,6 +543,7 @@ export const GENERATED_PLUGIN_SDK_FACADES = [
|
|||
{
|
||||
subpath: "line-runtime",
|
||||
source: pluginSource("line", "runtime-api.js"),
|
||||
loadPolicy: "activated",
|
||||
runtimeApiPreExportsPath: runtimeApiSourcePath("line"),
|
||||
typeExports: [
|
||||
"Action",
|
||||
|
|
@ -558,6 +572,8 @@ export const GENERATED_PLUGIN_SDK_FACADES = [
|
|||
{
|
||||
subpath: "line-surface",
|
||||
source: pluginSource("line", "runtime-api.js"),
|
||||
// This surface is also used by passive reply normalization helpers.
|
||||
// Keep it loadable without requiring the LINE plugin to be activated.
|
||||
exports: [
|
||||
"CardAction",
|
||||
"createActionCard",
|
||||
|
|
@ -611,6 +627,7 @@ export const GENERATED_PLUGIN_SDK_FACADES = [
|
|||
{
|
||||
subpath: "matrix-runtime-surface",
|
||||
source: pluginSource("matrix", "runtime-api.js"),
|
||||
loadPolicy: "activated",
|
||||
exports: ["resolveMatrixAccountStringValues", "setMatrixRuntime"],
|
||||
},
|
||||
{
|
||||
|
|
@ -850,6 +867,7 @@ export const GENERATED_PLUGIN_SDK_FACADES = [
|
|||
{
|
||||
subpath: "speech-runtime",
|
||||
source: pluginSource("speech-core", "runtime-api.js"),
|
||||
loadPolicy: "activated",
|
||||
exports: [
|
||||
"_test",
|
||||
"buildTtsSystemPromptHint",
|
||||
|
|
@ -931,6 +949,7 @@ export const GENERATED_PLUGIN_SDK_FACADES = [
|
|||
{
|
||||
subpath: "slack-runtime-surface",
|
||||
source: pluginSource("slack", "runtime-api.js"),
|
||||
loadPolicy: "activated",
|
||||
exports: [
|
||||
"handleSlackAction",
|
||||
"listSlackDirectoryGroupsLive",
|
||||
|
|
@ -1217,6 +1236,16 @@ export const GENERATED_PLUGIN_SDK_FACADES_BY_SUBPATH = Object.fromEntries(
|
|||
GENERATED_PLUGIN_SDK_FACADES.map((entry) => [entry.subpath, entry]),
|
||||
);
|
||||
|
||||
function resolveFacadeLoadPolicy(entry, sourcePath) {
|
||||
// Keep loader policy next to the facade entry itself so additions stay local
|
||||
// and mixed-source facades can opt into per-source behavior later if needed.
|
||||
const sourcePolicy = entry.sourceLoadPolicy?.[sourcePath];
|
||||
if (sourcePolicy) {
|
||||
return sourcePolicy;
|
||||
}
|
||||
return entry.loadPolicy ?? "plain";
|
||||
}
|
||||
|
||||
export const GENERATED_PLUGIN_SDK_FACADES_LABEL = "plugin-sdk-facades";
|
||||
export const GENERATED_PLUGIN_SDK_FACADES_SCRIPT = "scripts/generate-plugin-sdk-facades.mjs";
|
||||
export const GENERATED_PLUGIN_SDK_FACADE_TYPES_OUTPUT =
|
||||
|
|
@ -1515,25 +1544,40 @@ export function buildPluginSdkFacadeModule(entry, params = {}) {
|
|||
}
|
||||
}
|
||||
if (valueExports.length) {
|
||||
const runtimeImports = ["loadBundledPluginPublicSurfaceModuleSync"];
|
||||
const runtimeImports = new Set();
|
||||
if (needsLazyArrayHelper) {
|
||||
runtimeImports.unshift("createLazyFacadeArrayValue");
|
||||
runtimeImports.add("createLazyFacadeArrayValue");
|
||||
}
|
||||
if (needsLazyObjectHelper) {
|
||||
runtimeImports.unshift("createLazyFacadeObjectValue");
|
||||
runtimeImports.add("createLazyFacadeObjectValue");
|
||||
}
|
||||
lines.push(`import { ${runtimeImports.join(", ")} } from "./facade-runtime.js";`);
|
||||
for (const sourcePath of listFacadeEntrySourcePaths(entry)) {
|
||||
const loadPolicy = resolveFacadeLoadPolicy(entry, sourcePath);
|
||||
runtimeImports.add(
|
||||
loadPolicy === "activated"
|
||||
? "loadActivatedBundledPluginPublicSurfaceModuleSync"
|
||||
: "loadBundledPluginPublicSurfaceModuleSync",
|
||||
);
|
||||
}
|
||||
lines.push(
|
||||
`import { ${[...runtimeImports].toSorted((left, right) => left.localeCompare(right)).join(", ")} } from "./facade-runtime.js";`,
|
||||
);
|
||||
for (const [sourceIndex, sourcePath] of listFacadeEntrySourcePaths(entry).entries()) {
|
||||
if (!valueExportsBySource.has(sourcePath)) {
|
||||
continue;
|
||||
}
|
||||
const { dirName: sourceDirName, artifactBasename: sourceArtifactBasename } =
|
||||
normalizeFacadeSourceParts(sourcePath);
|
||||
const loadPolicy = resolveFacadeLoadPolicy(entry, sourcePath);
|
||||
const loaderName =
|
||||
loadPolicy === "activated"
|
||||
? "loadActivatedBundledPluginPublicSurfaceModuleSync"
|
||||
: "loadBundledPluginPublicSurfaceModuleSync";
|
||||
const loaderSuffix = sourceIndex === 0 ? "" : String(sourceIndex + 1);
|
||||
const moduleTypeName = sourceIndex === 0 ? "FacadeModule" : `FacadeModule${sourceIndex + 1}`;
|
||||
lines.push("");
|
||||
lines.push(`function loadFacadeModule${loaderSuffix}(): ${moduleTypeName} {`);
|
||||
lines.push(` return loadBundledPluginPublicSurfaceModuleSync<${moduleTypeName}>({`);
|
||||
lines.push(` return ${loaderName}<${moduleTypeName}>({`);
|
||||
lines.push(` dirName: ${JSON.stringify(sourceDirName)},`);
|
||||
lines.push(` artifactBasename: ${JSON.stringify(sourceArtifactBasename)},`);
|
||||
lines.push(" });");
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import { clearRuntimeConfigSnapshot, setRuntimeConfigSnapshot } from "../../config/config.js";
|
||||
import { resetPluginRuntimeStateForTest, setActivePluginRegistry } from "../../plugins/runtime.js";
|
||||
import { createTestRegistry } from "../../test-utils/channel-plugins.js";
|
||||
import { createSessionConversationTestRegistry } from "../../test-utils/session-conversation-registry.js";
|
||||
|
|
@ -14,6 +15,10 @@ describe("session conversation routing", () => {
|
|||
setActivePluginRegistry(createSessionConversationTestRegistry());
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
clearRuntimeConfigSnapshot();
|
||||
});
|
||||
|
||||
it("keeps generic :thread: parsing in core", () => {
|
||||
expect(
|
||||
resolveSessionConversationRef("agent:main:slack:channel:general:thread:1699999999.0001"),
|
||||
|
|
@ -51,6 +56,13 @@ describe("session conversation routing", () => {
|
|||
|
||||
it("keeps bundled Telegram topic parsing available before registry bootstrap", () => {
|
||||
resetPluginRuntimeStateForTest();
|
||||
setRuntimeConfigSnapshot({
|
||||
channels: {
|
||||
telegram: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(resolveSessionConversationRef("agent:main:telegram:group:-100123:topic:77")).toEqual({
|
||||
channel: "telegram",
|
||||
|
|
@ -66,6 +78,15 @@ describe("session conversation routing", () => {
|
|||
|
||||
it("keeps bundled Feishu parent fallbacks available before registry bootstrap", () => {
|
||||
resetPluginRuntimeStateForTest();
|
||||
setRuntimeConfigSnapshot({
|
||||
plugins: {
|
||||
entries: {
|
||||
feishu: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(
|
||||
resolveSessionConversationRef(
|
||||
|
|
@ -84,6 +105,21 @@ describe("session conversation routing", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it("does not load bundled session-key fallbacks for inactive channel plugins", () => {
|
||||
resetPluginRuntimeStateForTest();
|
||||
|
||||
expect(resolveSessionConversationRef("agent:main:telegram:group:-100123:topic:77")).toEqual({
|
||||
channel: "telegram",
|
||||
kind: "group",
|
||||
rawId: "-100123:topic:77",
|
||||
id: "-100123:topic:77",
|
||||
threadId: undefined,
|
||||
baseSessionKey: "agent:main:telegram:group:-100123:topic:77",
|
||||
baseConversationId: "-100123:topic:77",
|
||||
parentConversationCandidates: [],
|
||||
});
|
||||
});
|
||||
|
||||
it("lets Feishu own parent fallback candidates", () => {
|
||||
expect(
|
||||
resolveSessionConversationRef(
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { fileURLToPath } from "node:url";
|
||||
import { loadBundledPluginPublicSurfaceModuleSync } from "../../plugin-sdk/facade-runtime.js";
|
||||
import { tryLoadActivatedBundledPluginPublicSurfaceModuleSync } from "../../plugin-sdk/facade-runtime.js";
|
||||
import { resolveBundledPluginsDir } from "../../plugins/bundled-dir.js";
|
||||
import { resolveBundledPluginPublicSurfacePath } from "../../plugins/bundled-plugin-metadata.js";
|
||||
import {
|
||||
|
|
@ -150,12 +150,11 @@ function resolveBundledSessionConversationFallback(params: {
|
|||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const resolveSessionConversation =
|
||||
loadBundledPluginPublicSurfaceModuleSync<BundledSessionKeyModule>({
|
||||
tryLoadActivatedBundledPluginPublicSurfaceModuleSync<BundledSessionKeyModule>({
|
||||
dirName,
|
||||
artifactBasename: SESSION_KEY_API_ARTIFACT_BASENAME,
|
||||
}).resolveSessionConversation;
|
||||
})?.resolveSessionConversation;
|
||||
if (typeof resolveSessionConversation !== "function") {
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ vi.mock("../../config/sessions/paths.js", () => ({
|
|||
resolveSessionTranscriptsDirForAgent: mocks.resolveSessionTranscriptsDirForAgent,
|
||||
}));
|
||||
|
||||
vi.mock("../../browser/trash.js", () => ({
|
||||
vi.mock("../../plugin-sdk/browser-runtime.js", () => ({
|
||||
movePathToTrash: mocks.movePathToTrash,
|
||||
}));
|
||||
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ describe("plugin activation boundary", () => {
|
|||
DEFAULT_OPENCLAW_BROWSER_COLOR: typeof import("./plugin-sdk/browser-runtime.js").DEFAULT_OPENCLAW_BROWSER_COLOR;
|
||||
DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME: typeof import("./plugin-sdk/browser-runtime.js").DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME;
|
||||
DEFAULT_UPLOAD_DIR: typeof import("./plugin-sdk/browser-runtime.js").DEFAULT_UPLOAD_DIR;
|
||||
closeTrackedBrowserTabsForSessions: typeof import("./plugin-sdk/browser-runtime.js").closeTrackedBrowserTabsForSessions;
|
||||
redactCdpUrl: typeof import("./plugin-sdk/browser-runtime.js").redactCdpUrl;
|
||||
resolveBrowserConfig: typeof import("./plugin-sdk/browser-runtime.js").resolveBrowserConfig;
|
||||
resolveBrowserControlAuth: typeof import("./plugin-sdk/browser-runtime.js").resolveBrowserControlAuth;
|
||||
|
|
@ -41,6 +42,11 @@ describe("plugin activation boundary", () => {
|
|||
}>
|
||||
| undefined;
|
||||
let browserAmbientImportsPromise: Promise<void> | undefined;
|
||||
let discordMaintenancePromise:
|
||||
| Promise<{
|
||||
unbindThreadBindingsBySessionKey: typeof import("./plugin-sdk/discord-thread-bindings.js").unbindThreadBindingsBySessionKey;
|
||||
}>
|
||||
| undefined;
|
||||
|
||||
function importAmbientModules() {
|
||||
ambientImportsPromise ??= Promise.all([
|
||||
|
|
@ -77,6 +83,7 @@ describe("plugin activation boundary", () => {
|
|||
DEFAULT_OPENCLAW_BROWSER_COLOR: module.DEFAULT_OPENCLAW_BROWSER_COLOR,
|
||||
DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME: module.DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME,
|
||||
DEFAULT_UPLOAD_DIR: module.DEFAULT_UPLOAD_DIR,
|
||||
closeTrackedBrowserTabsForSessions: module.closeTrackedBrowserTabsForSessions,
|
||||
redactCdpUrl: module.redactCdpUrl,
|
||||
resolveBrowserConfig: module.resolveBrowserConfig,
|
||||
resolveBrowserControlAuth: module.resolveBrowserControlAuth,
|
||||
|
|
@ -96,6 +103,15 @@ describe("plugin activation boundary", () => {
|
|||
return browserAmbientImportsPromise;
|
||||
}
|
||||
|
||||
function importDiscordMaintenance() {
|
||||
discordMaintenancePromise ??= import("./plugin-sdk/discord-thread-bindings.js").then(
|
||||
(module) => ({
|
||||
unbindThreadBindingsBySessionKey: module.unbindThreadBindingsBySessionKey,
|
||||
}),
|
||||
);
|
||||
return discordMaintenancePromise;
|
||||
}
|
||||
|
||||
it("does not load bundled provider plugins on ambient command imports", async () => {
|
||||
await importAmbientModules();
|
||||
|
||||
|
|
@ -143,9 +159,30 @@ describe("plugin activation boundary", () => {
|
|||
cdpHost: "127.0.0.1",
|
||||
}),
|
||||
);
|
||||
expect(browser.redactCdpUrl("wss://user:secret@example.com/devtools/browser/123")).not.toContain(
|
||||
"secret",
|
||||
);
|
||||
expect(
|
||||
browser.redactCdpUrl("wss://user:secret@example.com/devtools/browser/123"),
|
||||
).not.toContain("secret");
|
||||
expect(loadBundledPluginPublicSurfaceModuleSync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("keeps browser cleanup helpers cold when browser is disabled", async () => {
|
||||
const browser = await importBrowserHelpers();
|
||||
|
||||
await expect(browser.closeTrackedBrowserTabsForSessions({ sessionKeys: [] })).resolves.toBe(0);
|
||||
expect(loadBundledPluginPublicSurfaceModuleSync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("keeps discord cleanup helpers cold when discord is disabled", async () => {
|
||||
const discord = await importDiscordMaintenance();
|
||||
|
||||
expect(
|
||||
discord.unbindThreadBindingsBySessionKey({
|
||||
targetSessionKey: "agent:main:test",
|
||||
targetKind: "acp",
|
||||
reason: "session-reset",
|
||||
sendFarewell: true,
|
||||
}),
|
||||
).toEqual([]);
|
||||
expect(loadBundledPluginPublicSurfaceModuleSync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,66 @@
|
|||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const runCommandWithTimeout = vi.hoisted(() => vi.fn());
|
||||
const mkdir = vi.hoisted(() => vi.fn());
|
||||
const access = vi.hoisted(() => vi.fn());
|
||||
const rename = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock("../process/exec.js", () => ({
|
||||
runCommandWithTimeout,
|
||||
}));
|
||||
|
||||
vi.mock("node:fs/promises", () => {
|
||||
const mocked = { mkdir, access, rename };
|
||||
return { ...mocked, default: mocked };
|
||||
});
|
||||
|
||||
vi.mock("node:os", () => ({
|
||||
default: {
|
||||
homedir: () => "/home/test",
|
||||
},
|
||||
homedir: () => "/home/test",
|
||||
}));
|
||||
|
||||
describe("browser maintenance", () => {
|
||||
beforeEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
runCommandWithTimeout.mockReset();
|
||||
mkdir.mockReset();
|
||||
access.mockReset();
|
||||
rename.mockReset();
|
||||
vi.spyOn(Date, "now").mockReturnValue(123);
|
||||
});
|
||||
|
||||
it("returns the target path when trash exits successfully", async () => {
|
||||
const { movePathToTrash } = await import("./browser-maintenance.js");
|
||||
runCommandWithTimeout.mockResolvedValue({
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
code: 0,
|
||||
signal: null,
|
||||
killed: false,
|
||||
termination: "exit",
|
||||
});
|
||||
|
||||
await expect(movePathToTrash("/tmp/demo")).resolves.toBe("/tmp/demo");
|
||||
expect(mkdir).not.toHaveBeenCalled();
|
||||
expect(rename).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("falls back to rename when trash exits non-zero", async () => {
|
||||
const { movePathToTrash } = await import("./browser-maintenance.js");
|
||||
runCommandWithTimeout.mockResolvedValue({
|
||||
stdout: "",
|
||||
stderr: "permission denied",
|
||||
code: 1,
|
||||
signal: null,
|
||||
killed: false,
|
||||
termination: "exit",
|
||||
});
|
||||
access.mockRejectedValue(new Error("missing"));
|
||||
|
||||
await expect(movePathToTrash("/tmp/demo")).resolves.toBe("/home/test/.Trash/demo-123");
|
||||
expect(mkdir).toHaveBeenCalledWith("/home/test/.Trash", { recursive: true });
|
||||
expect(rename).toHaveBeenCalledWith("/tmp/demo", "/home/test/.Trash/demo-123");
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
import { randomBytes } from "node:crypto";
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import type { PluginSdkFacadeTypeMap } from "../generated/plugin-sdk-facade-type-map.generated.js";
|
||||
import { runCommandWithTimeout } from "../process/exec.js";
|
||||
import { tryLoadActivatedBundledPluginPublicSurfaceModuleSync } from "./facade-runtime.js";
|
||||
|
||||
type BrowserRuntimeModule = PluginSdkFacadeTypeMap["browser-runtime"]["module"];
|
||||
|
||||
function createTrashCollisionSuffix(): string {
|
||||
return randomBytes(6).toString("hex");
|
||||
}
|
||||
|
||||
export const closeTrackedBrowserTabsForSessions: BrowserRuntimeModule["closeTrackedBrowserTabsForSessions"] =
|
||||
(async (...args) => {
|
||||
// Session reset always attempts browser cleanup, even when browser is disabled.
|
||||
// Keep that path a no-op unless the browser runtime is actually active.
|
||||
const closeTrackedTabs = tryLoadActivatedBundledPluginPublicSurfaceModuleSync<
|
||||
Pick<BrowserRuntimeModule, "closeTrackedBrowserTabsForSessions">
|
||||
>({
|
||||
dirName: "browser",
|
||||
artifactBasename: "runtime-api.js",
|
||||
})?.closeTrackedBrowserTabsForSessions;
|
||||
if (typeof closeTrackedTabs !== "function") {
|
||||
return 0;
|
||||
}
|
||||
return await closeTrackedTabs(...args);
|
||||
}) as BrowserRuntimeModule["closeTrackedBrowserTabsForSessions"];
|
||||
|
||||
export const movePathToTrash: BrowserRuntimeModule["movePathToTrash"] = (async (...args) => {
|
||||
const [targetPath] = args;
|
||||
try {
|
||||
const result = await runCommandWithTimeout(["trash", targetPath], { timeoutMs: 10_000 });
|
||||
if (result.code !== 0) {
|
||||
throw new Error(`trash exited with code ${result.code ?? "unknown"}`);
|
||||
}
|
||||
return targetPath;
|
||||
} catch {
|
||||
const trashDir = path.join(os.homedir(), ".Trash");
|
||||
await fs.mkdir(trashDir, { recursive: true });
|
||||
const base = path.basename(targetPath);
|
||||
const timestamp = Date.now();
|
||||
let destination = path.join(trashDir, `${base}-${timestamp}`);
|
||||
try {
|
||||
await fs.access(destination);
|
||||
destination = path.join(trashDir, `${base}-${timestamp}-${createTrashCollisionSuffix()}`);
|
||||
} catch {
|
||||
// The initial destination is free to use.
|
||||
}
|
||||
await fs.rename(targetPath, destination);
|
||||
return destination;
|
||||
}
|
||||
}) as BrowserRuntimeModule["movePathToTrash"];
|
||||
|
|
@ -13,13 +13,14 @@ export {
|
|||
resolveBrowserControlAuth,
|
||||
resolveProfile,
|
||||
} from "./browser-config.js";
|
||||
export { closeTrackedBrowserTabsForSessions, movePathToTrash } from "./browser-maintenance.js";
|
||||
import {
|
||||
createLazyFacadeObjectValue,
|
||||
loadBundledPluginPublicSurfaceModuleSync,
|
||||
loadActivatedBundledPluginPublicSurfaceModuleSync,
|
||||
} from "./facade-runtime.js";
|
||||
|
||||
function loadFacadeModule(): FacadeModule {
|
||||
return loadBundledPluginPublicSurfaceModuleSync<FacadeModule>({
|
||||
return loadActivatedBundledPluginPublicSurfaceModuleSync<FacadeModule>({
|
||||
dirName: "browser",
|
||||
artifactBasename: "runtime-api.js",
|
||||
});
|
||||
|
|
@ -71,11 +72,6 @@ export const browserTabAction: FacadeModule["browserTabAction"] = ((...args) =>
|
|||
loadFacadeModule()["browserTabAction"](...args)) as FacadeModule["browserTabAction"];
|
||||
export const browserTabs: FacadeModule["browserTabs"] = ((...args) =>
|
||||
loadFacadeModule()["browserTabs"](...args)) as FacadeModule["browserTabs"];
|
||||
export const closeTrackedBrowserTabsForSessions: FacadeModule["closeTrackedBrowserTabsForSessions"] =
|
||||
((...args) =>
|
||||
loadFacadeModule()["closeTrackedBrowserTabsForSessions"](
|
||||
...args,
|
||||
)) as FacadeModule["closeTrackedBrowserTabsForSessions"];
|
||||
export const createBrowserControlContext: FacadeModule["createBrowserControlContext"] = ((
|
||||
...args
|
||||
) =>
|
||||
|
|
@ -139,8 +135,6 @@ export const isPersistentBrowserProfileMutation: FacadeModule["isPersistentBrows
|
|||
loadFacadeModule()["isPersistentBrowserProfileMutation"](
|
||||
...args,
|
||||
)) as FacadeModule["isPersistentBrowserProfileMutation"];
|
||||
export const movePathToTrash: FacadeModule["movePathToTrash"] = ((...args) =>
|
||||
loadFacadeModule()["movePathToTrash"](...args)) as FacadeModule["movePathToTrash"];
|
||||
export const normalizeBrowserFormField: FacadeModule["normalizeBrowserFormField"] = ((...args) =>
|
||||
loadFacadeModule()["normalizeBrowserFormField"](
|
||||
...args,
|
||||
|
|
|
|||
|
|
@ -4,11 +4,11 @@ type FacadeEntry = PluginSdkFacadeTypeMap["browser"];
|
|||
type FacadeModule = FacadeEntry["module"];
|
||||
import {
|
||||
createLazyFacadeObjectValue,
|
||||
loadBundledPluginPublicSurfaceModuleSync,
|
||||
loadActivatedBundledPluginPublicSurfaceModuleSync,
|
||||
} from "./facade-runtime.js";
|
||||
|
||||
function loadFacadeModule(): FacadeModule {
|
||||
return loadBundledPluginPublicSurfaceModuleSync<FacadeModule>({
|
||||
return loadActivatedBundledPluginPublicSurfaceModuleSync<FacadeModule>({
|
||||
dirName: "browser",
|
||||
artifactBasename: "runtime-api.js",
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
import type { PluginSdkFacadeTypeMap } from "../generated/plugin-sdk-facade-type-map.generated.js";
|
||||
import { tryLoadActivatedBundledPluginPublicSurfaceModuleSync } from "./facade-runtime.js";
|
||||
|
||||
type DiscordThreadBindingsModule = PluginSdkFacadeTypeMap["discord-thread-bindings"]["module"];
|
||||
|
||||
export const unbindThreadBindingsBySessionKey: DiscordThreadBindingsModule["unbindThreadBindingsBySessionKey"] =
|
||||
((...args) => {
|
||||
// Session cleanup always attempts Discord thread unbinds, even when Discord is disabled.
|
||||
// Keep that path a no-op unless the Discord runtime is actually active.
|
||||
const unbindThreadBindings = tryLoadActivatedBundledPluginPublicSurfaceModuleSync<
|
||||
Pick<DiscordThreadBindingsModule, "unbindThreadBindingsBySessionKey">
|
||||
>({
|
||||
dirName: "discord",
|
||||
artifactBasename: "runtime-api.js",
|
||||
})?.unbindThreadBindingsBySessionKey;
|
||||
return typeof unbindThreadBindings === "function" ? unbindThreadBindings(...args) : [];
|
||||
}) as DiscordThreadBindingsModule["unbindThreadBindingsBySessionKey"];
|
||||
|
|
@ -4,11 +4,11 @@ type FacadeEntry = PluginSdkFacadeTypeMap["discord-runtime-surface"];
|
|||
type FacadeModule = FacadeEntry["module"];
|
||||
import {
|
||||
createLazyFacadeObjectValue,
|
||||
loadBundledPluginPublicSurfaceModuleSync,
|
||||
loadActivatedBundledPluginPublicSurfaceModuleSync,
|
||||
} from "./facade-runtime.js";
|
||||
|
||||
function loadFacadeModule(): FacadeModule {
|
||||
return loadBundledPluginPublicSurfaceModuleSync<FacadeModule>({
|
||||
return loadActivatedBundledPluginPublicSurfaceModuleSync<FacadeModule>({
|
||||
dirName: "discord",
|
||||
artifactBasename: "runtime-api.js",
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,10 +2,11 @@
|
|||
import type { PluginSdkFacadeTypeMap } from "../generated/plugin-sdk-facade-type-map.generated.js";
|
||||
type FacadeEntry = PluginSdkFacadeTypeMap["discord-thread-bindings"];
|
||||
type FacadeModule = FacadeEntry["module"];
|
||||
import { loadBundledPluginPublicSurfaceModuleSync } from "./facade-runtime.js";
|
||||
export { unbindThreadBindingsBySessionKey } from "./discord-maintenance.js";
|
||||
import { loadActivatedBundledPluginPublicSurfaceModuleSync } from "./facade-runtime.js";
|
||||
|
||||
function loadFacadeModule(): FacadeModule {
|
||||
return loadBundledPluginPublicSurfaceModuleSync<FacadeModule>({
|
||||
return loadActivatedBundledPluginPublicSurfaceModuleSync<FacadeModule>({
|
||||
dirName: "discord",
|
||||
artifactBasename: "runtime-api.js",
|
||||
});
|
||||
|
|
@ -61,12 +62,6 @@ export const setThreadBindingMaxAgeBySessionKey: FacadeModule["setThreadBindingM
|
|||
loadFacadeModule()["setThreadBindingMaxAgeBySessionKey"](
|
||||
...args,
|
||||
)) as FacadeModule["setThreadBindingMaxAgeBySessionKey"];
|
||||
export const unbindThreadBindingsBySessionKey: FacadeModule["unbindThreadBindingsBySessionKey"] = ((
|
||||
...args
|
||||
) =>
|
||||
loadFacadeModule()["unbindThreadBindingsBySessionKey"](
|
||||
...args,
|
||||
)) as FacadeModule["unbindThreadBindingsBySessionKey"];
|
||||
export type ThreadBindingManager = FacadeEntry["types"]["ThreadBindingManager"];
|
||||
export type ThreadBindingRecord = FacadeEntry["types"]["ThreadBindingRecord"];
|
||||
export type ThreadBindingTargetKind = FacadeEntry["types"]["ThreadBindingTargetKind"];
|
||||
|
|
|
|||
|
|
@ -2,7 +2,13 @@ import fs from "node:fs";
|
|||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { loadBundledPluginPublicSurfaceModuleSync } from "./facade-runtime.js";
|
||||
import { clearRuntimeConfigSnapshot, setRuntimeConfigSnapshot } from "../config/config.js";
|
||||
import {
|
||||
canLoadActivatedBundledPluginPublicSurface,
|
||||
loadActivatedBundledPluginPublicSurfaceModuleSync,
|
||||
loadBundledPluginPublicSurfaceModuleSync,
|
||||
tryLoadActivatedBundledPluginPublicSurfaceModuleSync,
|
||||
} from "./facade-runtime.js";
|
||||
|
||||
const tempDirs: string[] = [];
|
||||
const originalBundledPluginsDir = process.env.OPENCLAW_BUNDLED_PLUGINS_DIR;
|
||||
|
|
@ -63,6 +69,7 @@ function createCircularPluginDir(prefix: string): string {
|
|||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
clearRuntimeConfigSnapshot();
|
||||
if (originalBundledPluginsDir === undefined) {
|
||||
delete process.env.OPENCLAW_BUNDLED_PLUGINS_DIR;
|
||||
} else {
|
||||
|
|
@ -140,4 +147,69 @@ describe("plugin-sdk facade runtime", () => {
|
|||
}),
|
||||
).toThrow("plugin load failure");
|
||||
});
|
||||
|
||||
it("blocks runtime-api facade loads for bundled plugins that are not activated", () => {
|
||||
setRuntimeConfigSnapshot({});
|
||||
|
||||
expect(
|
||||
canLoadActivatedBundledPluginPublicSurface({
|
||||
dirName: "discord",
|
||||
artifactBasename: "runtime-api.js",
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(() =>
|
||||
loadActivatedBundledPluginPublicSurfaceModuleSync({
|
||||
dirName: "discord",
|
||||
artifactBasename: "runtime-api.js",
|
||||
}),
|
||||
).toThrow(/Bundled plugin public surface access blocked/);
|
||||
expect(
|
||||
tryLoadActivatedBundledPluginPublicSurfaceModuleSync({
|
||||
dirName: "discord",
|
||||
artifactBasename: "runtime-api.js",
|
||||
}),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it("allows runtime-api facade loads when the bundled plugin is explicitly enabled", () => {
|
||||
setRuntimeConfigSnapshot({
|
||||
plugins: {
|
||||
entries: {
|
||||
discord: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(
|
||||
canLoadActivatedBundledPluginPublicSurface({
|
||||
dirName: "discord",
|
||||
artifactBasename: "runtime-api.js",
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps shared runtime-core facades available without plugin activation", () => {
|
||||
setRuntimeConfigSnapshot({});
|
||||
|
||||
expect(
|
||||
canLoadActivatedBundledPluginPublicSurface({
|
||||
dirName: "speech-core",
|
||||
artifactBasename: "runtime-api.js",
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
canLoadActivatedBundledPluginPublicSurface({
|
||||
dirName: "image-generation-core",
|
||||
artifactBasename: "runtime-api.js",
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
canLoadActivatedBundledPluginPublicSurface({
|
||||
dirName: "media-understanding-core",
|
||||
artifactBasename: "runtime-api.js",
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,9 +2,16 @@ import fs from "node:fs";
|
|||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { createJiti } from "jiti";
|
||||
import { loadConfig, type OpenClawConfig } from "../config/config.js";
|
||||
import { applyPluginAutoEnable } from "../config/plugin-auto-enable.js";
|
||||
import { openBoundaryFileSync } from "../infra/boundary-file-read.js";
|
||||
import { resolveBundledPluginsDir } from "../plugins/bundled-dir.js";
|
||||
import { resolveBundledPluginPublicSurfacePath } from "../plugins/bundled-plugin-metadata.js";
|
||||
import { normalizePluginsConfig, resolveEffectiveEnableState } from "../plugins/config-state.js";
|
||||
import {
|
||||
loadPluginManifestRegistry,
|
||||
type PluginManifestRecord,
|
||||
} from "../plugins/manifest-registry.js";
|
||||
import {
|
||||
buildPluginLoaderAliasMap,
|
||||
buildPluginLoaderJitiOptions,
|
||||
|
|
@ -19,8 +26,21 @@ const OPENCLAW_PACKAGE_ROOT =
|
|||
}) ?? fileURLToPath(new URL("../..", import.meta.url));
|
||||
const CURRENT_MODULE_PATH = fileURLToPath(import.meta.url);
|
||||
const PUBLIC_SURFACE_SOURCE_EXTENSIONS = [".ts", ".mts", ".js", ".mjs", ".cts", ".cjs"] as const;
|
||||
const ALWAYS_ALLOWED_RUNTIME_DIR_NAMES = new Set([
|
||||
"image-generation-core",
|
||||
"media-understanding-core",
|
||||
"speech-core",
|
||||
]);
|
||||
const EMPTY_FACADE_BOUNDARY_CONFIG: OpenClawConfig = {};
|
||||
const jitiLoaders = new Map<string, ReturnType<typeof createJiti>>();
|
||||
const loadedFacadeModules = new Map<string, unknown>();
|
||||
let cachedBoundaryRawConfig: OpenClawConfig | undefined;
|
||||
let cachedBoundaryResolvedConfig:
|
||||
| {
|
||||
config: OpenClawConfig;
|
||||
normalizedPluginsConfig: ReturnType<typeof normalizePluginsConfig>;
|
||||
}
|
||||
| undefined;
|
||||
|
||||
function resolveSourceFirstPublicSurfacePath(params: {
|
||||
bundledPluginsDir?: string;
|
||||
|
|
@ -106,6 +126,88 @@ function getJiti(modulePath: string) {
|
|||
return loader;
|
||||
}
|
||||
|
||||
function readFacadeBoundaryConfigSafely(): OpenClawConfig {
|
||||
try {
|
||||
return loadConfig();
|
||||
} catch {
|
||||
return EMPTY_FACADE_BOUNDARY_CONFIG;
|
||||
}
|
||||
}
|
||||
|
||||
function getFacadeBoundaryResolvedConfig() {
|
||||
const rawConfig = readFacadeBoundaryConfigSafely();
|
||||
if (cachedBoundaryResolvedConfig && cachedBoundaryRawConfig === rawConfig) {
|
||||
return cachedBoundaryResolvedConfig;
|
||||
}
|
||||
|
||||
const config = applyPluginAutoEnable({
|
||||
config: rawConfig,
|
||||
env: process.env,
|
||||
}).config;
|
||||
const resolved = {
|
||||
config,
|
||||
normalizedPluginsConfig: normalizePluginsConfig(config.plugins),
|
||||
};
|
||||
cachedBoundaryRawConfig = rawConfig;
|
||||
cachedBoundaryResolvedConfig = resolved;
|
||||
return resolved;
|
||||
}
|
||||
|
||||
function resolveBundledPluginManifestRecordByDirName(dirName: string): PluginManifestRecord | null {
|
||||
const { config } = getFacadeBoundaryResolvedConfig();
|
||||
return (
|
||||
loadPluginManifestRegistry({
|
||||
config,
|
||||
cache: true,
|
||||
}).plugins.find(
|
||||
(plugin) => plugin.origin === "bundled" && path.basename(plugin.rootDir) === dirName,
|
||||
) ?? null
|
||||
);
|
||||
}
|
||||
|
||||
function resolveBundledPluginPublicSurfaceAccess(params: {
|
||||
dirName: string;
|
||||
artifactBasename: string;
|
||||
}): { allowed: boolean; pluginId?: string; reason?: string } {
|
||||
if (
|
||||
params.artifactBasename === "runtime-api.js" &&
|
||||
ALWAYS_ALLOWED_RUNTIME_DIR_NAMES.has(params.dirName)
|
||||
) {
|
||||
return {
|
||||
allowed: true,
|
||||
pluginId: params.dirName,
|
||||
};
|
||||
}
|
||||
|
||||
const manifestRecord = resolveBundledPluginManifestRecordByDirName(params.dirName);
|
||||
if (!manifestRecord) {
|
||||
return {
|
||||
allowed: false,
|
||||
reason: `no bundled plugin manifest found for ${params.dirName}`,
|
||||
};
|
||||
}
|
||||
const { config, normalizedPluginsConfig } = getFacadeBoundaryResolvedConfig();
|
||||
const enableState = resolveEffectiveEnableState({
|
||||
id: manifestRecord.id,
|
||||
origin: manifestRecord.origin,
|
||||
config: normalizedPluginsConfig,
|
||||
rootConfig: config,
|
||||
enabledByDefault: manifestRecord.enabledByDefault,
|
||||
});
|
||||
if (enableState.enabled) {
|
||||
return {
|
||||
allowed: true,
|
||||
pluginId: manifestRecord.id,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
allowed: false,
|
||||
pluginId: manifestRecord.id,
|
||||
reason: enableState.reason ?? "plugin runtime is not activated",
|
||||
};
|
||||
}
|
||||
|
||||
function createLazyFacadeValueLoader<T>(load: () => T): () => T {
|
||||
let loaded = false;
|
||||
let value: T;
|
||||
|
|
@ -220,3 +322,35 @@ export function loadBundledPluginPublicSurfaceModuleSync<T extends object>(param
|
|||
|
||||
return sentinel;
|
||||
}
|
||||
|
||||
export function canLoadActivatedBundledPluginPublicSurface(params: {
|
||||
dirName: string;
|
||||
artifactBasename: string;
|
||||
}): boolean {
|
||||
return resolveBundledPluginPublicSurfaceAccess(params).allowed;
|
||||
}
|
||||
|
||||
export function loadActivatedBundledPluginPublicSurfaceModuleSync<T extends object>(params: {
|
||||
dirName: string;
|
||||
artifactBasename: string;
|
||||
}): T {
|
||||
const access = resolveBundledPluginPublicSurfaceAccess(params);
|
||||
if (!access.allowed) {
|
||||
const pluginLabel = access.pluginId ?? params.dirName;
|
||||
throw new Error(
|
||||
`Bundled plugin public surface access blocked for "${pluginLabel}" via ${params.dirName}/${params.artifactBasename}: ${access.reason ?? "plugin runtime is not activated"}`,
|
||||
);
|
||||
}
|
||||
return loadBundledPluginPublicSurfaceModuleSync<T>(params);
|
||||
}
|
||||
|
||||
export function tryLoadActivatedBundledPluginPublicSurfaceModuleSync<T extends object>(params: {
|
||||
dirName: string;
|
||||
artifactBasename: string;
|
||||
}): T | null {
|
||||
const access = resolveBundledPluginPublicSurfaceAccess(params);
|
||||
if (!access.allowed) {
|
||||
return null;
|
||||
}
|
||||
return loadBundledPluginPublicSurfaceModuleSync<T>(params);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ import type { PluginSdkFacadeTypeMap } from "../generated/plugin-sdk-facade-type
|
|||
type FacadeEntry = PluginSdkFacadeTypeMap["feishu-conversation"];
|
||||
type FacadeModule = FacadeEntry["module"];
|
||||
import {
|
||||
createLazyFacadeObjectValue,
|
||||
createLazyFacadeArrayValue,
|
||||
createLazyFacadeObjectValue,
|
||||
loadBundledPluginPublicSurfaceModuleSync,
|
||||
} from "./facade-runtime.js";
|
||||
|
||||
|
|
|
|||
|
|
@ -2,10 +2,10 @@
|
|||
import type { PluginSdkFacadeTypeMap } from "../generated/plugin-sdk-facade-type-map.generated.js";
|
||||
type FacadeEntry = PluginSdkFacadeTypeMap["image-generation-runtime"];
|
||||
type FacadeModule = FacadeEntry["module"];
|
||||
import { loadBundledPluginPublicSurfaceModuleSync } from "./facade-runtime.js";
|
||||
import { loadActivatedBundledPluginPublicSurfaceModuleSync } from "./facade-runtime.js";
|
||||
|
||||
function loadFacadeModule(): FacadeModule {
|
||||
return loadBundledPluginPublicSurfaceModuleSync<FacadeModule>({
|
||||
return loadActivatedBundledPluginPublicSurfaceModuleSync<FacadeModule>({
|
||||
dirName: "image-generation-core",
|
||||
artifactBasename: "runtime-api.js",
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ import type { PluginSdkFacadeTypeMap } from "../generated/plugin-sdk-facade-type
|
|||
type FacadeEntry = PluginSdkFacadeTypeMap["kilocode"];
|
||||
type FacadeModule = FacadeEntry["module"];
|
||||
import {
|
||||
createLazyFacadeObjectValue,
|
||||
createLazyFacadeArrayValue,
|
||||
createLazyFacadeObjectValue,
|
||||
loadBundledPluginPublicSurfaceModuleSync,
|
||||
} from "./facade-runtime.js";
|
||||
|
||||
|
|
|
|||
|
|
@ -2,10 +2,10 @@
|
|||
import type { PluginSdkFacadeTypeMap } from "../generated/plugin-sdk-facade-type-map.generated.js";
|
||||
type FacadeEntry = PluginSdkFacadeTypeMap["line-runtime"];
|
||||
type FacadeModule = FacadeEntry["module"];
|
||||
import { loadBundledPluginPublicSurfaceModuleSync } from "./facade-runtime.js";
|
||||
import { loadActivatedBundledPluginPublicSurfaceModuleSync } from "./facade-runtime.js";
|
||||
|
||||
function loadFacadeModule(): FacadeModule {
|
||||
return loadBundledPluginPublicSurfaceModuleSync<FacadeModule>({
|
||||
return loadActivatedBundledPluginPublicSurfaceModuleSync<FacadeModule>({
|
||||
dirName: "line",
|
||||
artifactBasename: "runtime-api.js",
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,10 +2,10 @@
|
|||
import type { PluginSdkFacadeTypeMap } from "../generated/plugin-sdk-facade-type-map.generated.js";
|
||||
type FacadeEntry = PluginSdkFacadeTypeMap["matrix-runtime-surface"];
|
||||
type FacadeModule = FacadeEntry["module"];
|
||||
import { loadBundledPluginPublicSurfaceModuleSync } from "./facade-runtime.js";
|
||||
import { loadActivatedBundledPluginPublicSurfaceModuleSync } from "./facade-runtime.js";
|
||||
|
||||
function loadFacadeModule(): FacadeModule {
|
||||
return loadBundledPluginPublicSurfaceModuleSync<FacadeModule>({
|
||||
return loadActivatedBundledPluginPublicSurfaceModuleSync<FacadeModule>({
|
||||
dirName: "matrix",
|
||||
artifactBasename: "runtime-api.js",
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,10 +2,10 @@
|
|||
import type { PluginSdkFacadeTypeMap } from "../generated/plugin-sdk-facade-type-map.generated.js";
|
||||
type FacadeEntry = PluginSdkFacadeTypeMap["media-understanding-runtime"];
|
||||
type FacadeModule = FacadeEntry["module"];
|
||||
import { loadBundledPluginPublicSurfaceModuleSync } from "./facade-runtime.js";
|
||||
import { loadActivatedBundledPluginPublicSurfaceModuleSync } from "./facade-runtime.js";
|
||||
|
||||
function loadFacadeModule(): FacadeModule {
|
||||
return loadBundledPluginPublicSurfaceModuleSync<FacadeModule>({
|
||||
return loadActivatedBundledPluginPublicSurfaceModuleSync<FacadeModule>({
|
||||
dirName: "media-understanding-core",
|
||||
artifactBasename: "runtime-api.js",
|
||||
});
|
||||
|
|
|
|||
|
|
@ -4,11 +4,11 @@ type FacadeEntry = PluginSdkFacadeTypeMap["memory-core-engine-runtime"];
|
|||
type FacadeModule = FacadeEntry["module"];
|
||||
import {
|
||||
createLazyFacadeObjectValue,
|
||||
loadBundledPluginPublicSurfaceModuleSync,
|
||||
loadActivatedBundledPluginPublicSurfaceModuleSync,
|
||||
} from "./facade-runtime.js";
|
||||
|
||||
function loadFacadeModule(): FacadeModule {
|
||||
return loadBundledPluginPublicSurfaceModuleSync<FacadeModule>({
|
||||
return loadActivatedBundledPluginPublicSurfaceModuleSync<FacadeModule>({
|
||||
dirName: "memory-core",
|
||||
artifactBasename: "runtime-api.js",
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ import type { PluginSdkFacadeTypeMap } from "../generated/plugin-sdk-facade-type
|
|||
type FacadeEntry = PluginSdkFacadeTypeMap["minimax"];
|
||||
type FacadeModule = FacadeEntry["module"];
|
||||
import {
|
||||
createLazyFacadeObjectValue,
|
||||
createLazyFacadeArrayValue,
|
||||
createLazyFacadeObjectValue,
|
||||
loadBundledPluginPublicSurfaceModuleSync,
|
||||
} from "./facade-runtime.js";
|
||||
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ import type { PluginSdkFacadeTypeMap } from "../generated/plugin-sdk-facade-type
|
|||
type FacadeEntry = PluginSdkFacadeTypeMap["modelstudio"];
|
||||
type FacadeModule = FacadeEntry["module"];
|
||||
import {
|
||||
createLazyFacadeObjectValue,
|
||||
createLazyFacadeArrayValue,
|
||||
createLazyFacadeObjectValue,
|
||||
loadBundledPluginPublicSurfaceModuleSync,
|
||||
} from "./facade-runtime.js";
|
||||
|
||||
|
|
|
|||
|
|
@ -2,10 +2,10 @@
|
|||
import type { PluginSdkFacadeTypeMap } from "../generated/plugin-sdk-facade-type-map.generated.js";
|
||||
type FacadeEntry = PluginSdkFacadeTypeMap["slack-runtime-surface"];
|
||||
type FacadeModule = FacadeEntry["module"];
|
||||
import { loadBundledPluginPublicSurfaceModuleSync } from "./facade-runtime.js";
|
||||
import { loadActivatedBundledPluginPublicSurfaceModuleSync } from "./facade-runtime.js";
|
||||
|
||||
function loadFacadeModule(): FacadeModule {
|
||||
return loadBundledPluginPublicSurfaceModuleSync<FacadeModule>({
|
||||
return loadActivatedBundledPluginPublicSurfaceModuleSync<FacadeModule>({
|
||||
dirName: "slack",
|
||||
artifactBasename: "runtime-api.js",
|
||||
});
|
||||
|
|
|
|||
|
|
@ -4,11 +4,11 @@ type FacadeEntry = PluginSdkFacadeTypeMap["speech-runtime"];
|
|||
type FacadeModule = FacadeEntry["module"];
|
||||
import {
|
||||
createLazyFacadeObjectValue,
|
||||
loadBundledPluginPublicSurfaceModuleSync,
|
||||
loadActivatedBundledPluginPublicSurfaceModuleSync,
|
||||
} from "./facade-runtime.js";
|
||||
|
||||
function loadFacadeModule(): FacadeModule {
|
||||
return loadBundledPluginPublicSurfaceModuleSync<FacadeModule>({
|
||||
return loadActivatedBundledPluginPublicSurfaceModuleSync<FacadeModule>({
|
||||
dirName: "speech-core",
|
||||
artifactBasename: "runtime-api.js",
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const loadBundledPluginPublicSurfaceModuleSync = vi.hoisted(() => vi.fn());
|
||||
const loadActivatedBundledPluginPublicSurfaceModuleSync = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock("../plugin-sdk/facade-runtime.js", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("../plugin-sdk/facade-runtime.js")>();
|
||||
return {
|
||||
...actual,
|
||||
loadActivatedBundledPluginPublicSurfaceModuleSync,
|
||||
loadBundledPluginPublicSurfaceModuleSync,
|
||||
};
|
||||
});
|
||||
|
|
@ -14,6 +16,7 @@ describe("tts runtime facade", () => {
|
|||
let ttsModulePromise: Promise<typeof import("./tts.js")> | undefined;
|
||||
|
||||
beforeEach(() => {
|
||||
loadActivatedBundledPluginPublicSurfaceModuleSync.mockReset();
|
||||
loadBundledPluginPublicSurfaceModuleSync.mockReset();
|
||||
});
|
||||
|
||||
|
|
@ -30,15 +33,15 @@ describe("tts runtime facade", () => {
|
|||
|
||||
it("loads speech-core lazily on first runtime access", async () => {
|
||||
const buildTtsSystemPromptHint = vi.fn().mockReturnValue("hint");
|
||||
loadBundledPluginPublicSurfaceModuleSync.mockReturnValue({
|
||||
loadActivatedBundledPluginPublicSurfaceModuleSync.mockReturnValue({
|
||||
buildTtsSystemPromptHint,
|
||||
});
|
||||
|
||||
const tts = await importTtsModule();
|
||||
|
||||
expect(loadBundledPluginPublicSurfaceModuleSync).not.toHaveBeenCalled();
|
||||
expect(loadActivatedBundledPluginPublicSurfaceModuleSync).not.toHaveBeenCalled();
|
||||
expect(tts.buildTtsSystemPromptHint({} as never)).toBe("hint");
|
||||
expect(loadBundledPluginPublicSurfaceModuleSync).toHaveBeenCalledTimes(1);
|
||||
expect(loadActivatedBundledPluginPublicSurfaceModuleSync).toHaveBeenCalledTimes(1);
|
||||
expect(buildTtsSystemPromptHint).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue