Skip to content

Plugin SDK Refactor

Plugin SDK + Runtime Refactor Plan

Goal: every messaging connector is a plugin (bundled or external) using one stable API. No plugin imports from src/** directly. All dependencies go through the SDK or runtime.

Why now

  • Current connectors mix patterns: direct core imports, dist-only bridges, and custom helpers.
  • This makes upgrades brittle and blocks a clean external plugin surface.

Target architecture (two layers)

1) Plugin SDK (compile-time, stable, publishable)

Scope: types, helpers, and config utilities. No runtime state, no side effects.

Contents (examples):

  • Types: ChannelPlugin, adapters, ChannelMeta, ChannelCapabilities, ChannelDirectoryEntry.
  • Config helpers: buildChannelConfigSchema, setAccountEnabledInConfigSection, deleteAccountFromConfigSection, applyAccountNameToChannelSection.
  • Pairing helpers: PAIRING_APPROVED_MESSAGE, formatPairingApproveHint.
  • Onboarding helpers: promptChannelAccessConfig, addWildcardAllowFrom, onboarding types.
  • Tool param helpers: createActionGate, readStringParam, readNumberParam, readReactionParams, jsonResult.
  • Docs link helper: formatDocsLink.

Delivery:

  • Publish as openclaw/plugin-sdk (or export from core under openclaw/plugin-sdk).
  • Semver with explicit stability guarantees.

2) Plugin Runtime (execution surface, injected)

Scope: everything that touches core runtime behavior. Accessed via OpenClawPluginApi.runtime so plugins never import src/**.

Proposed surface (minimal but complete):

export type PluginRuntime = {
channel: {
text: {
chunkMarkdownText(text: string, limit: number): string[];
resolveTextChunkLimit(cfg: OpenClawConfig, channel: string, accountId?: string): number;
hasControlCommand(text: string, cfg: OpenClawConfig): boolean;
};
reply: {
dispatchReplyWithBufferedBlockDispatcher(params: {
ctx: unknown;
cfg: unknown;
dispatcherOptions: {
deliver: (payload: {
text?: string;
mediaUrls?: string[];
mediaUrl?: string;
}) => void | Promise<void>;
onError?: (err: unknown, info: { kind: string }) => void;
};
}): Promise<void>;
createReplyDispatcherWithTyping?: unknown; // adapter for Teams-style flows
};
routing: {
resolveAgentRoute(params: {
cfg: unknown;
channel: string;
accountId: string;
peer: { kind: "dm" | "group" | "channel"; id: string };
}): { sessionKey: string; accountId: string };
};
pairing: {
buildPairingReply(params: { channel: string; idLine: string; code: string }): string;
readAllowFromStore(channel: string): Promise<string[]>;
upsertPairingRequest(params: {
channel: string;
id: string;
meta?: { name?: string };
}): Promise<{ code: string; created: boolean }>;
};
media: {
fetchRemoteMedia(params: { url: string }): Promise<{ buffer: Buffer; contentType?: string }>;
saveMediaBuffer(
buffer: Uint8Array,
contentType: string | undefined,
direction: "inbound" | "outbound",
maxBytes: number,
): Promise<{ path: string; contentType?: string }>;
};
mentions: {
buildMentionRegexes(cfg: OpenClawConfig, agentId?: string): RegExp[];
matchesMentionPatterns(text: string, regexes: RegExp[]): boolean;
};
groups: {
resolveGroupPolicy(
cfg: OpenClawConfig,
channel: string,
accountId: string,
groupId: string,
): {
allowlistEnabled: boolean;
allowed: boolean;
groupConfig?: unknown;
defaultConfig?: unknown;
};
resolveRequireMention(
cfg: OpenClawConfig,
channel: string,
accountId: string,
groupId: string,
override?: boolean,
): boolean;
};
debounce: {
createInboundDebouncer<T>(opts: {
debounceMs: number;
buildKey: (v: T) => string | null;
shouldDebounce: (v: T) => boolean;
onFlush: (entries: T[]) => Promise<void>;
onError?: (err: unknown) => void;
}): { push: (v: T) => void; flush: () => Promise<void> };
resolveInboundDebounceMs(cfg: OpenClawConfig, channel: string): number;
};
commands: {
resolveCommandAuthorizedFromAuthorizers(params: {
useAccessGroups: boolean;
authorizers: Array<{ configured: boolean; allowed: boolean }>;
}): boolean;
};
};
logging: {
shouldLogVerbose(): boolean;
getChildLogger(name: string): PluginLogger;
};
state: {
resolveStateDir(cfg: OpenClawConfig): string;
};
};

Notes:

  • Runtime is the only way to access core behavior.
  • SDK is intentionally small and stable.
  • Each runtime method maps to an existing core implementation (no duplication).

Migration plan (phased, safe)

Phase 0: scaffolding

  • Introduce openclaw/plugin-sdk.
  • Add api.runtime to OpenClawPluginApi with the surface above.
  • Maintain existing imports during a transition window (deprecation warnings).

Phase 1: bridge cleanup (low risk)

  • Replace per-extension core-bridge.ts with api.runtime.
  • Migrate BlueBubbles, Zalo, Zalo Personal first (already close).
  • Remove duplicated bridge code.

Phase 2: light direct-import plugins

  • Migrate Matrix to SDK + runtime.
  • Validate onboarding, directory, group mention logic.

Phase 3: heavy direct-import plugins

  • Migrate MS Teams (largest set of runtime helpers).
  • Ensure reply/typing semantics match current behavior.

Phase 4: iMessage pluginization

  • Move iMessage into extensions/imessage.
  • Replace direct core calls with api.runtime.
  • Keep config keys, CLI behavior, and docs intact.

Phase 5: enforcement

  • Add lint rule / CI check: no extensions/** imports from src/**.
  • Add plugin SDK/version compatibility checks (runtime + SDK semver).

Compatibility and versioning

  • SDK: semver, published, documented changes.
  • Runtime: versioned per core release. Add api.runtime.version.
  • Plugins declare a required runtime range (e.g., openclawRuntime: ">=2026.2.0").

Testing strategy

  • Adapter-level unit tests (runtime functions exercised with real core implementation).
  • Golden tests per plugin: ensure no behavior drift (routing, pairing, allowlist, mention gating).
  • A single end-to-end plugin sample used in CI (install + run + smoke).

Open questions

  • Where to host SDK types: separate package or core export?
  • Runtime type distribution: in SDK (types only) or in core?
  • How to expose docs links for bundled vs external plugins?
  • Do we allow limited direct core imports for in-repo plugins during transition?

Success criteria

  • All channel connectors are plugins using SDK + runtime.
  • No extensions/** imports from src/**.
  • New connector templates depend only on SDK + runtime.
  • External plugins can be developed and updated without core source access.

Related docs: Plugins, Channels, Configuration.

Last updated: