fix(runtime): narrow bundled runtime startup surfaces

This commit is contained in:
Vincent Koc
2026-04-06 12:12:28 +01:00
parent 24eef3d6e3
commit 4133e3bb1d
7 changed files with 153 additions and 62 deletions

View File

@@ -1,3 +1,33 @@
import { loadBundledEntryExportSync } from "openclaw/plugin-sdk/channel-entry-contract";
type ChannelPluginModule = typeof import("./channel-plugin-runtime.js");
function createLazyObjectValue<T extends object>(load: () => T): T {
return new Proxy({} as T, {
get(_target, property, receiver) {
return Reflect.get(load(), property, receiver);
},
has(_target, property) {
return property in load();
},
ownKeys() {
return Reflect.ownKeys(load());
},
getOwnPropertyDescriptor(_target, property) {
const descriptor = Object.getOwnPropertyDescriptor(load(), property);
return descriptor ? { ...descriptor, configurable: true } : undefined;
},
});
}
function loadChannelPluginModule(): ChannelPluginModule {
return loadBundledEntryExportSync<ChannelPluginModule>(import.meta.url, {
specifier: "./channel-plugin-runtime.js",
});
}
// Keep bundled channel entry imports narrow so bootstrap/discovery paths do
// not drag the broader Mattermost helper surfaces into lightweight plugin loads.
export { mattermostPlugin } from "./src/channel.js";
export const mattermostPlugin: ChannelPluginModule["mattermostPlugin"] = createLazyObjectValue(
() => loadChannelPluginModule().mattermostPlugin as object,
) as ChannelPluginModule["mattermostPlugin"];

View File

@@ -0,0 +1,4 @@
// Private runtime-bearing plugin export for the bundled Mattermost entry.
// Keep the actual channel plugin value off the lighter channel-plugin-api seam
// so bootstrap can lazy-load it without tripping bundle init cycles.
export { mattermostPlugin } from "./src/channel.js";

View File

@@ -0,0 +1,9 @@
export {
listMattermostDirectoryGroups,
listMattermostDirectoryPeers,
} from "./mattermost/directory.js";
export { monitorMattermostProvider } from "./mattermost/monitor.js";
export { probeMattermost } from "./mattermost/probe.js";
export { addMattermostReaction, removeMattermostReaction } from "./mattermost/reactions.js";
export { sendMessageMattermost } from "./mattermost/send.js";
export { resolveMattermostOpaqueTarget } from "./mattermost/target-resolution.js";

View File

@@ -16,6 +16,7 @@ import { createLoggedPairingApprovalNotifier } from "openclaw/plugin-sdk/channel
import { createRestrictSendersChannelSecurity } from "openclaw/plugin-sdk/channel-policy";
import { createChannelDirectoryAdapter } from "openclaw/plugin-sdk/directory-runtime";
import { buildPassiveProbedChannelStatusSummary } from "openclaw/plugin-sdk/extension-shared";
import { createLazyRuntimeModule } from "openclaw/plugin-sdk/lazy-runtime";
import { isPrivateNetworkOptInEnabled } from "openclaw/plugin-sdk/ssrf-runtime";
import {
createComputedAccountStatusAdapter,
@@ -40,24 +41,43 @@ import {
resolveMattermostReplyToMode,
type ResolvedMattermostAccount,
} from "./mattermost/accounts.js";
import {
listMattermostDirectoryGroups,
listMattermostDirectoryPeers,
} from "./mattermost/directory.js";
import { monitorMattermostProvider } from "./mattermost/monitor.js";
import { probeMattermost } from "./mattermost/probe.js";
import { addMattermostReaction, removeMattermostReaction } from "./mattermost/reactions.js";
import { sendMessageMattermost } from "./mattermost/send.js";
import { collectMattermostSlashCallbackPaths } from "./mattermost/slash-commands.js";
import { resolveMattermostOpaqueTarget } from "./mattermost/target-resolution.js";
import type { MattermostSlashCommandConfig } from "./mattermost/slash-commands.js";
import { looksLikeMattermostTargetId, normalizeMattermostMessagingTarget } from "./normalize.js";
import { getMattermostRuntime } from "./runtime.js";
import { collectRuntimeConfigAssignments, secretTargetRegistryEntries } from "./secret-contract.js";
import { resolveMattermostOutboundSessionRoute } from "./session-route.js";
import { mattermostSetupAdapter } from "./setup-core.js";
import { mattermostSetupWizard } from "./setup-surface.js";
import type { MattermostConfig } from "./types.js";
const loadMattermostChannelRuntime = createLazyRuntimeModule(() => import("./channel.runtime.js"));
const DEFAULT_SLASH_CALLBACK_PATH = "/api/channels/mattermost/command";
function collectMattermostSlashCallbackPaths(
raw?: Partial<MattermostSlashCommandConfig>,
): string[] {
const callbackPath = (() => {
const trimmed = raw?.callbackPath?.trim();
if (!trimmed) {
return DEFAULT_SLASH_CALLBACK_PATH;
}
return trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
})();
const callbackUrl = raw?.callbackUrl?.trim();
const paths = new Set<string>([callbackPath]);
if (callbackUrl) {
try {
const pathname = new URL(callbackUrl).pathname;
if (pathname) {
paths.add(pathname);
}
} catch {
// Keep the normalized callback path when the configured URL is invalid.
}
}
return [...paths];
}
const mattermostSecurityAdapter = createRestrictSendersChannelSecurity<ResolvedMattermostAccount>({
channelKey: "mattermost",
resolveDmPolicy: (account) => account.config.dmPolicy,
@@ -135,7 +155,9 @@ const mattermostMessageActions: ChannelMessageActionAdapter = {
const { postId, emojiName, remove } = parseMattermostReactActionParams(params);
if (remove) {
const result = await removeMattermostReaction({
const result = await (
await loadMattermostChannelRuntime()
).removeMattermostReaction({
cfg,
postId,
emojiName,
@@ -152,7 +174,9 @@ const mattermostMessageActions: ChannelMessageActionAdapter = {
};
}
const result = await addMattermostReaction({
const result = await (
await loadMattermostChannelRuntime()
).addMattermostReaction({
cfg,
postId,
emojiName,
@@ -192,7 +216,9 @@ const mattermostMessageActions: ChannelMessageActionAdapter = {
const mediaUrl =
typeof params.media === "string" ? params.media.trim() || undefined : undefined;
const result = await sendMessageMattermost(to, message, {
const result = await (
await loadMattermostChannelRuntime()
).sendMessageMattermost(to, message, {
accountId: resolvedAccountId,
replyToId,
buttons: Array.isArray(params.buttons) ? params.buttons : undefined,
@@ -342,10 +368,14 @@ export const mattermostPlugin: ChannelPlugin<ResolvedMattermostAccount> = create
collectRuntimeConfigAssignments,
},
directory: createChannelDirectoryAdapter({
listGroups: async (params) => listMattermostDirectoryGroups(params),
listGroupsLive: async (params) => listMattermostDirectoryGroups(params),
listPeers: async (params) => listMattermostDirectoryPeers(params),
listPeersLive: async (params) => listMattermostDirectoryPeers(params),
listGroups: async (params) =>
(await loadMattermostChannelRuntime()).listMattermostDirectoryGroups(params),
listGroupsLive: async (params) =>
(await loadMattermostChannelRuntime()).listMattermostDirectoryGroups(params),
listPeers: async (params) =>
(await loadMattermostChannelRuntime()).listMattermostDirectoryPeers(params),
listPeersLive: async (params) =>
(await loadMattermostChannelRuntime()).listMattermostDirectoryPeers(params),
}),
messaging: {
defaultMarkdownTableMode: "off",
@@ -355,7 +385,9 @@ export const mattermostPlugin: ChannelPlugin<ResolvedMattermostAccount> = create
looksLikeId: looksLikeMattermostTargetId,
hint: "<channelId|user:ID|channel:ID>",
resolveTarget: async ({ cfg, accountId, input }) => {
const resolved = await resolveMattermostOpaqueTarget({
const resolved = await (
await loadMattermostChannelRuntime()
).resolveMattermostOpaqueTarget({
input,
cfg,
accountId,
@@ -389,12 +421,9 @@ export const mattermostPlugin: ChannelPlugin<ResolvedMattermostAccount> = create
if (!token || !baseUrl) {
return { ok: false, error: "bot token or baseUrl missing" };
}
return await probeMattermost(
baseUrl,
token,
timeoutMs,
isPrivateNetworkOptInEnabled(account.config),
);
return await (
await loadMattermostChannelRuntime()
).probeMattermost(baseUrl, token, timeoutMs, isPrivateNetworkOptInEnabled(account.config));
},
resolveAccountSnapshot: ({ account, runtime }) => ({
accountId: account.accountId,
@@ -450,7 +479,7 @@ export const mattermostPlugin: ChannelPlugin<ResolvedMattermostAccount> = create
botTokenSource: account.botTokenSource,
});
ctx.log?.info(`[${account.accountId}] starting channel`);
return monitorMattermostProvider({
return (await loadMattermostChannelRuntime()).monitorMattermostProvider({
botToken: account.botToken ?? undefined,
baseUrl: account.baseUrl ?? undefined,
accountId: account.accountId,
@@ -511,7 +540,9 @@ export const mattermostPlugin: ChannelPlugin<ResolvedMattermostAccount> = create
attachedResults: {
channel: "mattermost",
sendText: async ({ cfg, to, text, accountId, replyToId, threadId }) =>
await sendMessageMattermost(to, text, {
await (
await loadMattermostChannelRuntime()
).sendMessageMattermost(to, text, {
cfg,
accountId: accountId ?? undefined,
replyToId: replyToId ?? (threadId != null ? String(threadId) : undefined),
@@ -526,7 +557,9 @@ export const mattermostPlugin: ChannelPlugin<ResolvedMattermostAccount> = create
replyToId,
threadId,
}) =>
await sendMessageMattermost(to, text, {
await (
await loadMattermostChannelRuntime()
).sendMessageMattermost(to, text, {
cfg,
accountId: accountId ?? undefined,
mediaUrl,

View File

@@ -1,36 +1,19 @@
import { execFile } from "node:child_process";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { promisify } from "node:util";
import { describe, expect, it } from "vitest";
const execFileAsync = promisify(execFile);
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
const zaloRuntimeImportEnv = {
HOME: process.env.HOME,
NODE_OPTIONS: process.env.NODE_OPTIONS,
NODE_PATH: process.env.NODE_PATH,
PATH: process.env.PATH,
TERM: process.env.TERM,
} satisfies NodeJS.ProcessEnv;
import { loadRuntimeApiExportTypesViaJiti } from "../../test/helpers/plugins/jiti-runtime-api.js";
describe("zalo runtime api", () => {
it("exports the channel plugin without reentering setup surfaces", async () => {
const { stdout } = await execFileAsync(
process.execPath,
[
"--import",
"tsx",
"-e",
'const runtimeApi = await import("./extensions/zalo/runtime-api.ts"); process.stdout.write(runtimeApi.zaloPlugin.id);',
],
{
cwd: repoRoot,
env: zaloRuntimeImportEnv,
timeout: 40_000,
},
);
it("loads the narrow runtime api without reentering setup surfaces", () => {
const runtimeApiPath = path.join(process.cwd(), "extensions", "zalo", "runtime-api.ts");
expect(stdout).toBe("zalo");
}, 45_000);
expect(
loadRuntimeApiExportTypesViaJiti({
modulePath: runtimeApiPath,
exportNames: ["setZaloRuntime"],
realPluginSdkSpecifiers: ["openclaw/plugin-sdk/runtime-store"],
}),
).toEqual({
setZaloRuntime: "function",
});
});
});

View File

@@ -1,5 +1,4 @@
// Private runtime barrel for the bundled Zalo extension.
// Keep this barrel thin and free of local plugin self-imports so the bundled
// entry loader can resolve the channel plugin without re-entering this module.
export { zaloPlugin } from "./src/channel.js";
// Keep this barrel thin and free of channel plugin exports so direct runtime
// imports do not re-enter the full channel/setup surface.
export * from "./src/runtime-api.js";

View File

@@ -1,4 +1,6 @@
import { iterateBootstrapChannelPlugins } from "../channels/plugins/bootstrap-registry.js";
import { loadBundledPluginPublicSurfaceModuleSync } from "../plugin-sdk/facade-runtime.js";
import { listBundledPluginMetadata } from "../plugins/bundled-plugin-metadata.js";
import type { SecretTargetRegistryEntry } from "./target-registry-types.js";
const SECRET_INPUT_SHAPE = "secret_input"; // pragma: allowlist secret
@@ -6,7 +8,38 @@ const SIBLING_REF_SHAPE = "sibling_ref"; // pragma: allowlist secret
function listChannelSecretTargetRegistryEntries(): SecretTargetRegistryEntry[] {
const entries: SecretTargetRegistryEntry[] = [];
const handledChannelIds = new Set<string>();
for (const metadata of listBundledPluginMetadata({
includeChannelConfigs: false,
includeSyntheticChannelConfigs: false,
})) {
const channelIds = metadata.manifest.channels ?? [];
if (channelIds.length === 0) {
continue;
}
if (!metadata.publicSurfaceArtifacts?.includes("contract-api.js")) {
continue;
}
try {
const contractApi = loadBundledPluginPublicSurfaceModuleSync<{
secretTargetRegistryEntries?: readonly SecretTargetRegistryEntry[];
}>({
dirName: metadata.dirName,
artifactBasename: "contract-api.js",
});
entries.push(...(contractApi.secretTargetRegistryEntries ?? []));
channelIds.forEach((channelId) => handledChannelIds.add(channelId));
} catch {
// Fall back to the full bootstrap plugin surface for channels that do not
// expose a usable secret contract artifact.
}
}
for (const plugin of iterateBootstrapChannelPlugins()) {
if (handledChannelIds.has(plugin.id)) {
continue;
}
entries.push(...(plugin.secrets?.secretTargetRegistryEntries ?? []));
}
return entries;