refactor: tighten plugin sdk entry surface

This commit is contained in:
Peter Steinberger
2026-03-21 20:07:24 +00:00
parent c29ba9d21a
commit bfcfc17a8b
49 changed files with 937 additions and 774 deletions

View File

@@ -1,7 +1,7 @@
// Narrow plugin-sdk surface for the bundled copilot-proxy plugin.
// Keep this list additive and scoped to symbols used under extensions/copilot-proxy.
export { definePluginEntry } from "./core.js";
export { definePluginEntry } from "./plugin-entry.js";
export type {
OpenClawPluginApi,
ProviderAuthContext,

View File

@@ -1,21 +1,29 @@
import {
createScopedAccountReplyToModeResolver,
createTopLevelChannelReplyToModeResolver,
} from "../channels/plugins/threading-helpers.js";
import type {
ChannelOutboundAdapter,
ChannelPairingAdapter,
ChannelSecurityAdapter,
} from "../channels/plugins/types.adapters.js";
import type {
ChannelMessagingAdapter,
ChannelOutboundSessionRoute,
ChannelThreadingAdapter,
} from "../channels/plugins/types.core.js";
import type { ChannelPlugin } from "../channels/plugins/types.plugin.js";
import { getChatChannelMeta } from "../channels/registry.js";
import type { OpenClawConfig } from "../config/config.js";
import type { ReplyToMode } from "../config/types.base.js";
import { buildOutboundBaseSessionKey } from "../infra/outbound/base-session-key.js";
import { emptyPluginConfigSchema } from "../plugins/config-schema.js";
import type { PluginRuntime } from "../plugins/runtime/types.js";
import type {
OpenClawPluginApi,
OpenClawPluginCommandDefinition,
OpenClawPluginConfigSchema,
OpenClawPluginDefinition,
PluginCommandContext,
PluginInteractiveTelegramHandlerContext,
} from "../plugins/types.js";
import type { OpenClawPluginApi, OpenClawPluginConfigSchema } from "../plugins/types.js";
import { createScopedDmSecurityResolver } from "./channel-config-helpers.js";
import { createTextPairingAdapter } from "./channel-pairing.js";
import { createAttachedChannelResultAdapter } from "./channel-send-result.js";
import { definePluginEntry } from "./plugin-entry.js";
export type {
AnyAgentTool,
@@ -77,6 +85,7 @@ export type { OpenClawPluginApi } from "../plugins/types.js";
export type { PluginRuntime } from "../plugins/runtime/types.js";
export { emptyPluginConfigSchema } from "../plugins/config-schema.js";
export { definePluginEntry } from "./plugin-entry.js";
export { delegateCompactionToRuntime } from "../context-engine/delegate.js";
export { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../routing/session-key.js";
export { buildChannelConfigSchema } from "../channels/plugins/config-schema.js";
@@ -177,28 +186,11 @@ type DefineChannelPluginEntryOptions<TPlugin extends ChannelPlugin = ChannelPlug
name: string;
description: string;
plugin: TPlugin;
configSchema?: DefinePluginEntryOptions["configSchema"];
configSchema?: OpenClawPluginConfigSchema | (() => OpenClawPluginConfigSchema);
setRuntime?: (runtime: PluginRuntime) => void;
registerFull?: (api: OpenClawPluginApi) => void;
};
type DefinePluginEntryOptions = {
id: string;
name: string;
description: string;
kind?: OpenClawPluginDefinition["kind"];
configSchema?: OpenClawPluginConfigSchema | (() => OpenClawPluginConfigSchema);
register: (api: OpenClawPluginApi) => void;
};
type DefinedPluginEntry = {
id: string;
name: string;
description: string;
configSchema: OpenClawPluginConfigSchema;
register: NonNullable<OpenClawPluginDefinition["register"]>;
} & Pick<OpenClawPluginDefinition, "kind">;
type CreateChannelPluginBaseOptions<TResolvedAccount> = {
id: ChannelPlugin<TResolvedAccount>["id"];
meta?: Partial<NonNullable<ChannelPlugin<TResolvedAccount>["meta"]>>;
@@ -235,31 +227,6 @@ type CreatedChannelPluginBase<TResolvedAccount> = Pick<
>
>;
function resolvePluginConfigSchema(
configSchema: DefinePluginEntryOptions["configSchema"] = emptyPluginConfigSchema,
): OpenClawPluginConfigSchema {
return typeof configSchema === "function" ? configSchema() : configSchema;
}
// Shared generic plugin-entry boilerplate for bundled and third-party plugins.
export function definePluginEntry({
id,
name,
description,
kind,
configSchema = emptyPluginConfigSchema,
register,
}: DefinePluginEntryOptions): DefinedPluginEntry {
return {
id,
name,
description,
...(kind ? { kind } : {}),
configSchema: resolvePluginConfigSchema(configSchema),
register,
};
}
// Shared channel-plugin entry boilerplate for bundled and third-party channels.
export function defineChannelPluginEntry<TPlugin extends ChannelPlugin>({
id,
@@ -291,6 +258,161 @@ export function defineSetupPluginEntry<TPlugin>(plugin: TPlugin) {
return { plugin };
}
type ChatChannelPluginBase<TResolvedAccount, Probe, Audit> = Omit<
ChannelPlugin<TResolvedAccount, Probe, Audit>,
"security" | "pairing" | "threading" | "outbound"
> &
Partial<
Pick<
ChannelPlugin<TResolvedAccount, Probe, Audit>,
"security" | "pairing" | "threading" | "outbound"
>
>;
type ChatChannelSecurityOptions<TResolvedAccount extends { accountId?: string | null }> = {
dm: {
channelKey: string;
resolvePolicy: (account: TResolvedAccount) => string | null | undefined;
resolveAllowFrom: (account: TResolvedAccount) => Array<string | number> | null | undefined;
resolveFallbackAccountId?: (account: TResolvedAccount) => string | null | undefined;
defaultPolicy?: string;
allowFromPathSuffix?: string;
policyPathSuffix?: string;
approveChannelId?: string;
approveHint?: string;
normalizeEntry?: (raw: string) => string;
};
collectWarnings?: ChannelSecurityAdapter<TResolvedAccount>["collectWarnings"];
};
type ChatChannelPairingOptions = {
text: {
idLabel: string;
message: string;
normalizeAllowEntry?: ChannelPairingAdapter["normalizeAllowEntry"];
notify: Parameters<typeof createTextPairingAdapter>[0]["notify"];
};
};
type ChatChannelThreadingReplyModeOptions<TResolvedAccount> =
| { topLevelReplyToMode: string }
| {
scopedAccountReplyToMode: {
resolveAccount: (cfg: OpenClawConfig, accountId?: string | null) => TResolvedAccount;
resolveReplyToMode: (
account: TResolvedAccount,
chatType?: string | null,
) => ReplyToMode | null | undefined;
fallback?: ReplyToMode;
};
}
| {
resolveReplyToMode: NonNullable<ChannelThreadingAdapter["resolveReplyToMode"]>;
};
type ChatChannelThreadingOptions<TResolvedAccount> =
ChatChannelThreadingReplyModeOptions<TResolvedAccount> &
Omit<ChannelThreadingAdapter, "resolveReplyToMode">;
type ChatChannelAttachedOutboundOptions = {
base: Omit<ChannelOutboundAdapter, "sendText" | "sendMedia" | "sendPoll">;
attachedResults: Parameters<typeof createAttachedChannelResultAdapter>[0];
};
function resolveChatChannelSecurity<TResolvedAccount extends { accountId?: string | null }>(
security:
| ChannelSecurityAdapter<TResolvedAccount>
| ChatChannelSecurityOptions<TResolvedAccount>
| undefined,
): ChannelSecurityAdapter<TResolvedAccount> | undefined {
if (!security) {
return undefined;
}
if (!("dm" in security)) {
return security;
}
return {
resolveDmPolicy: createScopedDmSecurityResolver<TResolvedAccount>(security.dm),
...(security.collectWarnings ? { collectWarnings: security.collectWarnings } : {}),
};
}
function resolveChatChannelPairing(
pairing: ChannelPairingAdapter | ChatChannelPairingOptions | undefined,
): ChannelPairingAdapter | undefined {
if (!pairing) {
return undefined;
}
if (!("text" in pairing)) {
return pairing;
}
return createTextPairingAdapter(pairing.text);
}
function resolveChatChannelThreading<TResolvedAccount>(
threading: ChannelThreadingAdapter | ChatChannelThreadingOptions<TResolvedAccount> | undefined,
): ChannelThreadingAdapter | undefined {
if (!threading) {
return undefined;
}
if (!("topLevelReplyToMode" in threading) && !("scopedAccountReplyToMode" in threading)) {
return threading;
}
let resolveReplyToMode: ChannelThreadingAdapter["resolveReplyToMode"];
if ("topLevelReplyToMode" in threading) {
resolveReplyToMode = createTopLevelChannelReplyToModeResolver(threading.topLevelReplyToMode);
} else {
resolveReplyToMode = createScopedAccountReplyToModeResolver<TResolvedAccount>(
threading.scopedAccountReplyToMode,
);
}
return {
...threading,
resolveReplyToMode,
};
}
function resolveChatChannelOutbound(
outbound: ChannelOutboundAdapter | ChatChannelAttachedOutboundOptions | undefined,
): ChannelOutboundAdapter | undefined {
if (!outbound) {
return undefined;
}
if (!("attachedResults" in outbound)) {
return outbound;
}
return {
...outbound.base,
...createAttachedChannelResultAdapter(outbound.attachedResults),
};
}
// Shared higher-level builder for chat-style channels that mostly compose
// scoped DM security, text pairing, reply threading, and attached send results.
export function createChatChannelPlugin<
TResolvedAccount extends { accountId?: string | null },
Probe = unknown,
Audit = unknown,
>(params: {
base: ChatChannelPluginBase<TResolvedAccount, Probe, Audit>;
security?:
| ChannelSecurityAdapter<TResolvedAccount>
| ChatChannelSecurityOptions<TResolvedAccount>;
pairing?: ChannelPairingAdapter | ChatChannelPairingOptions;
threading?: ChannelThreadingAdapter | ChatChannelThreadingOptions<TResolvedAccount>;
outbound?: ChannelOutboundAdapter | ChatChannelAttachedOutboundOptions;
}): ChannelPlugin<TResolvedAccount, Probe, Audit> {
return {
...params.base,
...(params.security ? { security: resolveChatChannelSecurity(params.security) } : {}),
...(params.pairing ? { pairing: resolveChatChannelPairing(params.pairing) } : {}),
...(params.threading ? { threading: resolveChatChannelThreading(params.threading) } : {}),
...(params.outbound ? { outbound: resolveChatChannelOutbound(params.outbound) } : {}),
} as ChannelPlugin<TResolvedAccount, Probe, Audit>;
}
// Shared base object for channel plugins that only need to override a few optional surfaces.
export function createChannelPluginBase<TResolvedAccount>(
params: CreateChannelPluginBaseOptions<TResolvedAccount>,

View File

@@ -1,7 +1,7 @@
// Narrow plugin-sdk surface for the bundled diffs plugin.
// Keep this list additive and scoped to symbols used under extensions/diffs.
export { definePluginEntry } from "./core.js";
export { definePluginEntry } from "./plugin-entry.js";
export type { OpenClawConfig } from "../config/config.js";
export { resolvePreferredOpenClawTmpDir } from "../infra/tmp-openclaw-dir.js";
export type {

View File

@@ -1,7 +1,7 @@
// Narrow plugin-sdk surface for the bundled llm-task plugin.
// Keep this list additive and scoped to symbols used under extensions/llm-task.
export { definePluginEntry } from "./core.js";
export { definePluginEntry } from "./plugin-entry.js";
export { resolvePreferredOpenClawTmpDir } from "../infra/tmp-openclaw-dir.js";
export {
formatThinkingLevels,

View File

@@ -1,7 +1,7 @@
// Private Lobster plugin helpers for bundled extensions.
// Keep this surface narrow and limited to the Lobster workflow/tool contract.
export { definePluginEntry } from "./core.js";
export { definePluginEntry } from "./plugin-entry.js";
export {
applyWindowsSpawnProgramPolicy,
materializeWindowsSpawnProgram,

View File

@@ -1,5 +1,5 @@
// Narrow plugin-sdk surface for the bundled memory-lancedb plugin.
// Keep this list additive and scoped to symbols used under extensions/memory-lancedb.
export { definePluginEntry } from "./core.js";
export { definePluginEntry } from "./plugin-entry.js";
export type { OpenClawPluginApi } from "../plugins/types.js";

View File

@@ -1,5 +1,5 @@
// Narrow plugin-sdk surface for the bundled open-prose plugin.
// Keep this list additive and scoped to symbols used under extensions/open-prose.
export { definePluginEntry } from "./core.js";
export { definePluginEntry } from "./plugin-entry.js";
export type { OpenClawPluginApi } from "../plugins/types.js";

View File

@@ -1,7 +1,7 @@
// Narrow plugin-sdk surface for the bundled phone-control plugin.
// Keep this list additive and scoped to symbols used under extensions/phone-control.
export { definePluginEntry } from "./core.js";
export { definePluginEntry } from "./plugin-entry.js";
export type {
OpenClawPluginApi,
OpenClawPluginCommandDefinition,

View File

@@ -0,0 +1,33 @@
import { readdirSync, readFileSync } from "node:fs";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { describe, expect, it } from "vitest";
const ROOT_DIR = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const REPO_ROOT = resolve(ROOT_DIR, "..");
const EXTENSIONS_DIR = resolve(REPO_ROOT, "extensions");
const CORE_PLUGIN_ENTRY_IMPORT_RE =
/import\s*\{[^}]*\bdefinePluginEntry\b[^}]*\}\s*from\s*"openclaw\/plugin-sdk\/core"/;
describe("plugin entry guardrails", () => {
it("keeps bundled extension entry modules off direct definePluginEntry imports from core", () => {
const failures: string[] = [];
for (const entry of readdirSync(EXTENSIONS_DIR, { withFileTypes: true })) {
if (!entry.isDirectory()) {
continue;
}
const indexPath = resolve(EXTENSIONS_DIR, entry.name, "index.ts");
try {
const source = readFileSync(indexPath, "utf8");
if (CORE_PLUGIN_ENTRY_IMPORT_RE.test(source)) {
failures.push(`extensions/${entry.name}/index.ts`);
}
} catch {
// Skip extensions without index.ts entry modules.
}
}
expect(failures).toEqual([]);
});
});

View File

@@ -39,6 +39,7 @@ import * as lazyRuntimeSdk from "openclaw/plugin-sdk/lazy-runtime";
import * as matrixRuntimeSharedSdk from "openclaw/plugin-sdk/matrix-runtime-shared";
import * as mediaRuntimeSdk from "openclaw/plugin-sdk/media-runtime";
import * as ollamaSetupSdk from "openclaw/plugin-sdk/ollama-setup";
import * as pluginEntrySdk from "openclaw/plugin-sdk/plugin-entry";
import * as providerAuthSdk from "openclaw/plugin-sdk/provider-auth";
import * as providerModelsSdk from "openclaw/plugin-sdk/provider-models";
import * as providerSetupSdk from "openclaw/plugin-sdk/provider-setup";
@@ -140,6 +141,7 @@ describe("plugin-sdk subpath exports", () => {
expect(typeof coreSdk.definePluginEntry).toBe("function");
expect(typeof coreSdk.defineChannelPluginEntry).toBe("function");
expect(typeof coreSdk.defineSetupPluginEntry).toBe("function");
expect(typeof coreSdk.createChatChannelPlugin).toBe("function");
expect(typeof coreSdk.createChannelPluginBase).toBe("function");
expect(typeof coreSdk.isSecretRef).toBe("function");
expect(typeof coreSdk.optionalStringEnum).toBe("function");
@@ -148,6 +150,10 @@ describe("plugin-sdk subpath exports", () => {
expect("registerSandboxBackend" in asExports(coreSdk)).toBe(false);
});
it("re-exports the canonical plugin entry helper from core", () => {
expect(coreSdk.definePluginEntry).toBe(pluginEntrySdk.definePluginEntry);
});
it("exports routing helpers from the dedicated subpath", () => {
expect(typeof routingSdk.buildAgentSessionKey).toBe("function");
expect(typeof routingSdk.resolveThreadSessionKeys).toBe("function");

View File

@@ -1,6 +1,6 @@
// Narrow plugin-sdk surface for the bundled thread-ownership plugin.
// Keep this list additive and scoped to symbols used under extensions/thread-ownership.
export { definePluginEntry } from "./core.js";
export { definePluginEntry } from "./plugin-entry.js";
export type { OpenClawConfig } from "../config/config.js";
export type { OpenClawPluginApi } from "../plugins/types.js";

View File

@@ -1,7 +1,7 @@
// Private helper surface for the bundled voice-call plugin.
// Keep this surface narrow and limited to the voice-call feature contract.
export { definePluginEntry } from "./core.js";
export { definePluginEntry } from "./plugin-entry.js";
export {
TtsAutoSchema,
TtsConfigSchema,