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 underopenclaw/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.runtimetoOpenClawPluginApiwith the surface above. - Maintain existing imports during a transition window (deprecation warnings).
Phase 1: bridge cleanup (low risk)
- Replace per-extension
core-bridge.tswithapi.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 fromsrc/**. - 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 fromsrc/**. - New connector templates depend only on SDK + runtime.
- External plugins can be developed and updated without core source access.
Related docs: Plugins, Channels, Configuration.