refactor: narrow extension public seams

This commit is contained in:
Peter Steinberger 2026-03-17 09:58:12 -07:00
parent bdf2c265a7
commit 6d9bf6de93
No known key found for this signature in database
19 changed files with 109 additions and 39 deletions

View File

@ -957,6 +957,16 @@ authoring plugins:
- `openclaw/plugin-sdk/compat` remains as a legacy migration surface for older
external plugins. Bundled plugins should not use it, and non-test imports emit
a one-time deprecation warning outside test environments.
- Bundled extension internals remain private. External plugins should use only
`openclaw/plugin-sdk/*` subpaths. OpenClaw core/test code may use the repo
public seams under `extensions/<id>/index.js`, `api.js`, `runtime-api.js`,
`setup-entry.js`, and narrowly scoped files such as `login-qr-api.js`. Never
import `extensions/<id>/src/*` from core or from another extension.
- Repo seam split:
`extensions/<id>/api.js` is the helper/types barrel,
`extensions/<id>/runtime-api.js` is the runtime-only barrel,
`extensions/<id>/index.js` is the bundled plugin entry,
and `extensions/<id>/setup-entry.js` is the setup plugin entry.
- `openclaw/plugin-sdk/telegram` for Telegram channel plugin types and shared channel-facing helpers. Built-in Telegram implementation internals stay private to the bundled extension.
- `openclaw/plugin-sdk/discord` for Discord channel plugin types and shared channel-facing helpers. Built-in Discord implementation internals stay private to the bundled extension.
- `openclaw/plugin-sdk/slack` for Slack channel plugin types and shared channel-facing helpers. Built-in Slack implementation internals stay private to the bundled extension.

View File

@ -1,4 +1,3 @@
export * from "./runtime-api.js";
export * from "./src/account-inspect.js";
export * from "./src/accounts.js";
export * from "./src/actions/handle-action.guild-admin.js";

View File

@ -1,4 +1,3 @@
export * from "./runtime-api.js";
export * from "./src/accounts.js";
export * from "./src/target-parsing-helpers.js";
export * from "./src/targets.js";

View File

@ -1,2 +1 @@
export * from "./runtime-api.js";
export * from "./src/accounts.js";

View File

@ -1,4 +1,3 @@
export * from "./runtime-api.js";
export * from "./src/account-inspect.js";
export * from "./src/accounts.js";
export * from "./src/actions.js";

View File

@ -1,4 +1,3 @@
export * from "./runtime-api.js";
export * from "./src/account-inspect.js";
export * from "./src/accounts.js";
export * from "./src/allow-from.js";

View File

@ -1,2 +1 @@
export * from "./runtime-api.js";
export * from "./src/accounts.js";

View File

@ -101,7 +101,7 @@ export function getWebSessionMocks(): AnyMocks {
return webSessionMocks;
}
vi.mock("../../extensions/whatsapp/api.js", () => webSessionMocks);
vi.mock("../../extensions/whatsapp/runtime-api.js", () => webSessionMocks);
export const MAIN_SESSION_KEY = "agent:main:main";

View File

@ -2,10 +2,10 @@ import { expect, vi } from "vitest";
import {
__testing as discordThreadBindingTesting,
createThreadBindingManager as createDiscordThreadBindingManager,
} from "../../../../extensions/discord/api.js";
} from "../../../../extensions/discord/runtime-api.js";
import { createFeishuThreadBindingManager } from "../../../../extensions/feishu/api.js";
import { setMatrixRuntime } from "../../../../extensions/matrix/api.js";
import { createTelegramThreadBindingManager } from "../../../../extensions/telegram/api.js";
import { createTelegramThreadBindingManager } from "../../../../extensions/telegram/runtime-api.js";
import type { OpenClawConfig } from "../../../config/config.js";
import {
getSessionBindingService,

View File

@ -574,7 +574,7 @@ vi.mock("../commands/health.js", () => ({
vi.mock("../commands/status.js", () => ({
getStatusSummary: vi.fn().mockResolvedValue({ ok: true }),
}));
vi.mock("../../extensions/whatsapp/api.js", () => ({
vi.mock("../../extensions/whatsapp/runtime-api.js", () => ({
sendMessageWhatsApp: (...args: unknown[]) =>
(hoisted.sendWhatsAppMock as (...args: unknown[]) => unknown)(...args),
sendPollWhatsApp: (...args: unknown[]) =>

View File

@ -4,6 +4,36 @@ import { fileURLToPath } from "node:url";
import { describe, expect, it } from "vitest";
const ROOT_DIR = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const ALLOWED_EXTENSION_PUBLIC_SEAMS = new Set([
"api.js",
"index.js",
"login-qr-api.js",
"runtime-api.js",
"setup-entry.js",
]);
const GUARDED_CHANNEL_EXTENSIONS = new Set([
"bluebubbles",
"discord",
"feishu",
"googlechat",
"imessage",
"irc",
"line",
"matrix",
"mattermost",
"msteams",
"nextcloud-talk",
"nostr",
"signal",
"slack",
"synology-chat",
"telegram",
"tlon",
"twitch",
"whatsapp",
"zalo",
"zalouser",
]);
type GuardedSource = {
path: string;
@ -186,6 +216,27 @@ function collectCoreSourceFiles(): string[] {
return files;
}
function collectExtensionImports(text: string): string[] {
return [...text.matchAll(/["']([^"']*extensions\/[^"']+\.(?:[cm]?[jt]sx?))["']/g)].map(
(match) => match[1] ?? "",
);
}
function expectOnlyApprovedExtensionSeams(file: string, imports: string[]): void {
for (const specifier of imports) {
const normalized = specifier.replaceAll("\\", "/");
const extensionId = normalized.match(/extensions\/([^/]+)\//)?.[1] ?? null;
if (!extensionId || !GUARDED_CHANNEL_EXTENSIONS.has(extensionId)) {
continue;
}
const basename = normalized.split("/").at(-1) ?? "";
expect(
ALLOWED_EXTENSION_PUBLIC_SEAMS.has(basename),
`${file} should only import approved extension seams, got ${specifier}`,
).toBe(true);
}
}
describe("channel import guardrails", () => {
it("keeps channel helper modules off their own SDK barrels", () => {
for (const source of SAME_CHANNEL_SDK_GUARDS) {
@ -236,4 +287,16 @@ describe("channel import guardrails", () => {
);
}
});
it("keeps core extension imports limited to approved public seams", () => {
for (const file of collectCoreSourceFiles()) {
expectOnlyApprovedExtensionSeams(file, collectExtensionImports(readFileSync(file, "utf8")));
}
});
it("keeps extension-to-extension imports limited to approved public seams", () => {
for (const file of collectExtensionSourceFiles()) {
expectOnlyApprovedExtensionSeams(file, collectExtensionImports(readFileSync(file, "utf8")));
}
});
});

View File

@ -13,7 +13,7 @@ export type {
ThreadBindingManager,
ThreadBindingRecord,
ThreadBindingTargetKind,
} from "../../extensions/discord/api.js";
} from "../../extensions/discord/runtime-api.js";
export type {
ChannelConfiguredBindingProvider,
ChannelConfiguredBindingConversationRef,
@ -75,20 +75,20 @@ export {
normalizeDiscordMessagingTarget,
normalizeDiscordOutboundTarget,
} from "../../extensions/discord/api.js";
export { collectDiscordAuditChannelIds } from "../../extensions/discord/api.js";
export { collectDiscordAuditChannelIds } from "../../extensions/discord/runtime-api.js";
export { collectDiscordStatusIssues } from "../../extensions/discord/api.js";
export {
DISCORD_DEFAULT_INBOUND_WORKER_TIMEOUT_MS,
DISCORD_DEFAULT_LISTENER_TIMEOUT_MS,
} from "../../extensions/discord/api.js";
} from "../../extensions/discord/runtime-api.js";
export { normalizeExplicitDiscordSessionKey } from "../../extensions/discord/api.js";
export {
autoBindSpawnedDiscordSubagent,
listThreadBindingsBySessionKey,
unbindThreadBindingsBySessionKey,
} from "../../extensions/discord/api.js";
export { getGateway } from "../../extensions/discord/api.js";
export { getPresence } from "../../extensions/discord/api.js";
} from "../../extensions/discord/runtime-api.js";
export { getGateway } from "../../extensions/discord/runtime-api.js";
export { getPresence } from "../../extensions/discord/runtime-api.js";
export { readDiscordComponentSpec } from "../../extensions/discord/api.js";
export { resolveDiscordChannelId } from "../../extensions/discord/api.js";
export {
@ -134,5 +134,5 @@ export {
unpinMessageDiscord,
uploadEmojiDiscord,
uploadStickerDiscord,
} from "../../extensions/discord/api.js";
export { discordMessageActions } from "../../extensions/discord/api.js";
} from "../../extensions/discord/runtime-api.js";
export { discordMessageActions } from "../../extensions/discord/runtime-api.js";

View File

@ -42,4 +42,4 @@ export { IMessageConfigSchema } from "../config/zod-schema.providers-core.js";
export { resolveChannelMediaMaxBytes } from "../channels/plugins/media-limits.js";
export { collectStatusIssuesFromLastError } from "./status-helpers.js";
export { sendMessageIMessage } from "../../extensions/imessage/api.js";
export { sendMessageIMessage } from "../../extensions/imessage/runtime-api.js";

View File

@ -52,6 +52,6 @@ export {
listSignalAccountIds,
resolveDefaultSignalAccountId,
} from "../../extensions/signal/api.js";
export { resolveSignalReactionLevel } from "../../extensions/signal/api.js";
export { removeReactionSignal, sendReactionSignal } from "../../extensions/signal/api.js";
export { sendMessageSignal } from "../../extensions/signal/api.js";
export { resolveSignalReactionLevel } from "../../extensions/signal/runtime-api.js";
export { removeReactionSignal, sendReactionSignal } from "../../extensions/signal/runtime-api.js";
export { sendMessageSignal } from "../../extensions/signal/runtime-api.js";

View File

@ -60,7 +60,7 @@ export { extractSlackToolSend, listSlackMessageActions } from "../../extensions/
export { buildSlackThreadingToolContext } from "../../extensions/slack/api.js";
export { parseSlackBlocksInput } from "../../extensions/slack/api.js";
export { handleSlackHttpRequest } from "../../extensions/slack/api.js";
export { sendMessageSlack } from "../../extensions/slack/api.js";
export { sendMessageSlack } from "../../extensions/slack/runtime-api.js";
export {
deleteSlackMessage,
downloadSlackFile,

View File

@ -19,7 +19,7 @@ export type {
} from "../channels/plugins/types.adapters.js";
export type { InspectedTelegramAccount } from "../../extensions/telegram/api.js";
export type { ResolvedTelegramAccount } from "../../extensions/telegram/api.js";
export type { TelegramProbe } from "../../extensions/telegram/api.js";
export type { TelegramProbe } from "../../extensions/telegram/runtime-api.js";
export type { TelegramButtonStyle, TelegramInlineButtons } from "../../extensions/telegram/api.js";
export type { StickerMetadata } from "../../extensions/telegram/api.js";
@ -96,10 +96,10 @@ export {
sendMessageTelegram,
sendPollTelegram,
sendStickerTelegram,
} from "../../extensions/telegram/api.js";
} from "../../extensions/telegram/runtime-api.js";
export { getCacheStats, searchStickers } from "../../extensions/telegram/api.js";
export { resolveTelegramToken } from "../../extensions/telegram/api.js";
export { telegramMessageActions } from "../../extensions/telegram/api.js";
export { resolveTelegramToken } from "../../extensions/telegram/runtime-api.js";
export { telegramMessageActions } from "../../extensions/telegram/runtime-api.js";
export { collectTelegramStatusIssues } from "../../extensions/telegram/api.js";
export { sendTelegramPayloadMessages } from "../../extensions/telegram/api.js";
export {

View File

@ -1,8 +1,11 @@
export type { ChannelMessageActionName } from "../channels/plugins/types.js";
export type { OpenClawConfig } from "../config/config.js";
export type { DmPolicy, GroupPolicy, WhatsAppAccountConfig } from "../config/types.js";
export type { WebChannelStatus, WebMonitorTuning } from "../../extensions/whatsapp/api.js";
export type { WebInboundMessage, WebListenerCloseReason } from "../../extensions/whatsapp/api.js";
export type { WebChannelStatus, WebMonitorTuning } from "../../extensions/whatsapp/runtime-api.js";
export type {
WebInboundMessage,
WebListenerCloseReason,
} from "../../extensions/whatsapp/runtime-api.js";
export type {
ChannelMessageActionContext,
ChannelPlugin,
@ -73,7 +76,7 @@ export {
logoutWeb,
pickWebChannel,
webAuthExists,
} from "../../extensions/whatsapp/api.js";
} from "../../extensions/whatsapp/runtime-api.js";
export {
DEFAULT_WEB_MEDIA_BYTES,
HEARTBEAT_PROMPT,
@ -81,28 +84,28 @@ export {
monitorWebChannel,
resolveHeartbeatRecipients,
runWebHeartbeatOnce,
} from "../../extensions/whatsapp/api.js";
} from "../../extensions/whatsapp/runtime-api.js";
export {
extractMediaPlaceholder,
extractText,
monitorWebInbox,
} from "../../extensions/whatsapp/api.js";
export { loginWeb } from "../../extensions/whatsapp/api.js";
} from "../../extensions/whatsapp/runtime-api.js";
export { loginWeb } from "../../extensions/whatsapp/runtime-api.js";
export {
getDefaultLocalRoots,
loadWebMedia,
loadWebMediaRaw,
optimizeImageToJpeg,
} from "../../extensions/whatsapp/api.js";
} from "../../extensions/whatsapp/runtime-api.js";
export {
sendMessageWhatsApp,
sendPollWhatsApp,
sendReactionWhatsApp,
} from "../../extensions/whatsapp/api.js";
} from "../../extensions/whatsapp/runtime-api.js";
export {
createWaSocket,
formatError,
getStatusCode,
waitForWaConnection,
} from "../../extensions/whatsapp/api.js";
export { createWhatsAppLoginTool } from "../../extensions/whatsapp/api.js";
} from "../../extensions/whatsapp/runtime-api.js";
export { createWhatsAppLoginTool } from "../../extensions/whatsapp/runtime-api.js";

View File

@ -1,4 +1,4 @@
import { collectTelegramUnmentionedGroupIds } from "../../../extensions/telegram/api.js";
import { collectTelegramUnmentionedGroupIds } from "../../../extensions/telegram/runtime-api.js";
import { telegramMessageActions } from "../../../extensions/telegram/runtime-api.js";
import {
setTelegramThreadBindingIdleTimeoutBySessionKey,

View File

@ -141,7 +141,7 @@ export type PluginRuntimeChannel = {
};
telegram: {
auditGroupMembership: typeof import("../../../extensions/telegram/runtime-api.js").auditTelegramGroupMembership;
collectUnmentionedGroupIds: typeof import("../../../extensions/telegram/api.js").collectTelegramUnmentionedGroupIds;
collectUnmentionedGroupIds: typeof import("../../../extensions/telegram/runtime-api.js").collectTelegramUnmentionedGroupIds;
probeTelegram: typeof import("../../../extensions/telegram/runtime-api.js").probeTelegram;
resolveTelegramToken: typeof import("../../../extensions/telegram/runtime-api.js").resolveTelegramToken;
sendMessageTelegram: typeof import("../../../extensions/telegram/runtime-api.js").sendMessageTelegram;