mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-02 20:20:21 +00:00
refactor: tighten plugin sdk entry surface
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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,
|
||||
|
||||
33
src/plugin-sdk/plugin-entry-guardrails.test.ts
Normal file
33
src/plugin-sdk/plugin-entry-guardrails.test.ts
Normal 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([]);
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user