mirror of https://github.com/openclaw/openclaw.git
Plugin SDK: guard package subpaths and fix Twitch setup export
* fix(plugins): add missing secret-input-schema build entry and Matrix runtime export buildSecretInputSchema was not included in plugin-sdk-entrypoints.json, so it was never emitted to dist/plugin-sdk/secret-input-schema.js. This caused a ReferenceError during onboard when configuring channels that use secret input schemas (matrix, feishu, mattermost, bluebubbles, nextcloud-talk, zalo). Additionally, the Matrix extension's hand-written runtime-api barrel was missing the re-export, unlike other extensions that use `export *` from their plugin-sdk subpath. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Plugin SDK: guard package subpaths and fix Twitch setup export * Plugin SDK: fix import guardrail drift --------- Co-authored-by: hxy91819 <masonxhuang@icloud.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
8ac4b09fa4
commit
3d31ba7830
|
|
@ -4,15 +4,20 @@ import {
|
|||
toDirectoryEntries,
|
||||
type DirectoryConfigParams,
|
||||
} from "openclaw/plugin-sdk/directory-runtime";
|
||||
import type { InspectedDiscordAccount } from "../../../src/channels/read-only-account-inspect.discord.runtime.js";
|
||||
import { inspectReadOnlyChannelAccount } from "../../../src/channels/read-only-account-inspect.js";
|
||||
import { inspectDiscordAccount } from "../api.js";
|
||||
import type { InspectedDiscordAccount } from "../api.js";
|
||||
|
||||
export async function listDiscordDirectoryPeersFromConfig(params: DirectoryConfigParams) {
|
||||
const account = (await inspectReadOnlyChannelAccount({
|
||||
channelId: "discord",
|
||||
function inspectDiscordDirectoryAccount(
|
||||
params: DirectoryConfigParams,
|
||||
): InspectedDiscordAccount | null {
|
||||
return inspectDiscordAccount({
|
||||
cfg: params.cfg,
|
||||
accountId: params.accountId,
|
||||
})) as InspectedDiscordAccount | null;
|
||||
});
|
||||
}
|
||||
|
||||
export async function listDiscordDirectoryPeersFromConfig(params: DirectoryConfigParams) {
|
||||
const account = inspectDiscordDirectoryAccount(params);
|
||||
if (!account || !("config" in account)) {
|
||||
return [];
|
||||
}
|
||||
|
|
@ -34,11 +39,7 @@ export async function listDiscordDirectoryPeersFromConfig(params: DirectoryConfi
|
|||
}
|
||||
|
||||
export async function listDiscordDirectoryGroupsFromConfig(params: DirectoryConfigParams) {
|
||||
const account = (await inspectReadOnlyChannelAccount({
|
||||
channelId: "discord",
|
||||
cfg: params.cfg,
|
||||
accountId: params.accountId,
|
||||
})) as InspectedDiscordAccount | null;
|
||||
const account = inspectDiscordDirectoryAccount(params);
|
||||
if (!account || !("config" in account)) {
|
||||
return [];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { normalizeSlackMessagingTarget } from "openclaw/plugin-sdk/channel-runtime";
|
||||
import {
|
||||
applyDirectoryQueryAndLimit,
|
||||
collectNormalizedDirectoryIds,
|
||||
|
|
@ -5,16 +6,18 @@ import {
|
|||
toDirectoryEntries,
|
||||
type DirectoryConfigParams,
|
||||
} from "openclaw/plugin-sdk/directory-runtime";
|
||||
import { normalizeSlackMessagingTarget } from "../../../src/channels/plugins/normalize/slack.js";
|
||||
import { inspectReadOnlyChannelAccount } from "../../../src/channels/read-only-account-inspect.js";
|
||||
import type { InspectedSlackAccount } from "../../../src/channels/read-only-account-inspect.slack.runtime.js";
|
||||
import { inspectSlackAccount } from "../api.js";
|
||||
import type { InspectedSlackAccount } from "../api.js";
|
||||
|
||||
export async function listSlackDirectoryPeersFromConfig(params: DirectoryConfigParams) {
|
||||
const account = (await inspectReadOnlyChannelAccount({
|
||||
channelId: "slack",
|
||||
function inspectSlackDirectoryAccount(params: DirectoryConfigParams): InspectedSlackAccount | null {
|
||||
return inspectSlackAccount({
|
||||
cfg: params.cfg,
|
||||
accountId: params.accountId,
|
||||
})) as InspectedSlackAccount | null;
|
||||
});
|
||||
}
|
||||
|
||||
export async function listSlackDirectoryPeersFromConfig(params: DirectoryConfigParams) {
|
||||
const account = inspectSlackDirectoryAccount(params);
|
||||
if (!account || !("config" in account)) {
|
||||
return [];
|
||||
}
|
||||
|
|
@ -40,11 +43,7 @@ export async function listSlackDirectoryPeersFromConfig(params: DirectoryConfigP
|
|||
}
|
||||
|
||||
export async function listSlackDirectoryGroupsFromConfig(params: DirectoryConfigParams) {
|
||||
const account = (await inspectReadOnlyChannelAccount({
|
||||
channelId: "slack",
|
||||
cfg: params.cfg,
|
||||
accountId: params.accountId,
|
||||
})) as InspectedSlackAccount | null;
|
||||
const account = inspectSlackDirectoryAccount(params);
|
||||
if (!account || !("config" in account)) {
|
||||
return [];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
|
||||
import type { ChannelMessageActionContext } from "openclaw/plugin-sdk/channel-runtime";
|
||||
import { normalizeInteractiveReply } from "openclaw/plugin-sdk/interactive-runtime";
|
||||
import { readNumberParam, readStringParam } from "../../../src/agents/tools/common.js";
|
||||
import { readNumberParam, readStringParam } from "openclaw/plugin-sdk/slack-core";
|
||||
import { parseSlackBlocksInput } from "./blocks-input.js";
|
||||
import { buildSlackInteractiveBlocks } from "./blocks-render.js";
|
||||
|
||||
|
|
|
|||
|
|
@ -6,15 +6,20 @@ import {
|
|||
toDirectoryEntries,
|
||||
type DirectoryConfigParams,
|
||||
} from "openclaw/plugin-sdk/directory-runtime";
|
||||
import { inspectReadOnlyChannelAccount } from "../../../src/channels/read-only-account-inspect.js";
|
||||
import type { InspectedTelegramAccount } from "../../../src/channels/read-only-account-inspect.telegram.runtime.js";
|
||||
import { inspectTelegramAccount } from "../api.js";
|
||||
import type { InspectedTelegramAccount } from "../api.js";
|
||||
|
||||
export async function listTelegramDirectoryPeersFromConfig(params: DirectoryConfigParams) {
|
||||
const account = (await inspectReadOnlyChannelAccount({
|
||||
channelId: "telegram",
|
||||
async function inspectTelegramDirectoryAccount(
|
||||
params: DirectoryConfigParams,
|
||||
): Promise<InspectedTelegramAccount | null> {
|
||||
return inspectTelegramAccount({
|
||||
cfg: params.cfg,
|
||||
accountId: params.accountId,
|
||||
})) as InspectedTelegramAccount | null;
|
||||
});
|
||||
}
|
||||
|
||||
export async function listTelegramDirectoryPeersFromConfig(params: DirectoryConfigParams) {
|
||||
const account = await inspectTelegramDirectoryAccount(params);
|
||||
if (!account || !("config" in account)) {
|
||||
return [];
|
||||
}
|
||||
|
|
@ -36,11 +41,7 @@ export async function listTelegramDirectoryPeersFromConfig(params: DirectoryConf
|
|||
}
|
||||
|
||||
export async function listTelegramDirectoryGroupsFromConfig(params: DirectoryConfigParams) {
|
||||
const account = (await inspectReadOnlyChannelAccount({
|
||||
channelId: "telegram",
|
||||
cfg: params.cfg,
|
||||
accountId: params.accountId,
|
||||
})) as InspectedTelegramAccount | null;
|
||||
const account = await inspectTelegramDirectoryAccount(params);
|
||||
if (!account || !("config" in account)) {
|
||||
return [];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ import {
|
|||
listDirectoryUserEntriesFromAllowFrom,
|
||||
type DirectoryConfigParams,
|
||||
} from "openclaw/plugin-sdk/directory-runtime";
|
||||
import { isWhatsAppGroupJid, normalizeWhatsAppTarget } from "../../../src/whatsapp/normalize.js";
|
||||
import { resolveWhatsAppAccount } from "./accounts.js";
|
||||
import { isWhatsAppGroupJid, normalizeWhatsAppTarget } from "./normalize.js";
|
||||
|
||||
export async function listWhatsAppDirectoryPeersFromConfig(params: DirectoryConfigParams) {
|
||||
const account = resolveWhatsAppAccount({ cfg: params.cfg, accountId: params.accountId });
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
export {
|
||||
isWhatsAppGroupJid,
|
||||
looksLikeWhatsAppTargetId,
|
||||
normalizeWhatsAppAllowFromEntries,
|
||||
normalizeWhatsAppMessagingTarget,
|
||||
normalizeWhatsAppTarget,
|
||||
} from "openclaw/plugin-sdk/channel-runtime";
|
||||
|
|
|
|||
|
|
@ -252,7 +252,10 @@ function collectCoreSourceFiles(): string[] {
|
|||
fullPath.includes(".test.") ||
|
||||
fullPath.includes(".spec.") ||
|
||||
fullPath.includes(".fixture.") ||
|
||||
fullPath.includes(".snap")
|
||||
fullPath.includes(".snap") ||
|
||||
// src/plugin-sdk is the curated bridge layer; validate its contracts with dedicated
|
||||
// plugin-sdk guardrails instead of the generic "core should not touch extensions" rule.
|
||||
fullPath.includes(`${resolve(ROOT_DIR, "plugin-sdk")}/`)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ export * from "../channels/plugins/whatsapp-heartbeat.js";
|
|||
export * from "../infra/outbound/send-deps.js";
|
||||
export * from "../polls.js";
|
||||
export * from "../utils/message-channel.js";
|
||||
export * from "../whatsapp/normalize.js";
|
||||
export { createActionGate, jsonResult, readStringParam } from "../agents/tools/common.js";
|
||||
export * from "./channel-lifecycle.js";
|
||||
export * from "./directory-runtime.js";
|
||||
|
|
|
|||
|
|
@ -0,0 +1,145 @@
|
|||
import { readdirSync, readFileSync } from "node:fs";
|
||||
import { dirname, relative, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { pluginSdkEntrypoints } from "./entrypoints.js";
|
||||
|
||||
const ROOT_DIR = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const REPO_ROOT = resolve(ROOT_DIR, "..");
|
||||
const REFERENCE_SCAN_ROOTS = ["src", "extensions", "scripts", "test", "docs"] as const;
|
||||
const PLUGIN_SDK_SUBPATH_PATTERN = /openclaw\/plugin-sdk\/([a-z0-9][a-z0-9-]*)\b/g;
|
||||
|
||||
function collectPluginSdkPackageExports(): string[] {
|
||||
const packageJson = JSON.parse(readFileSync(resolve(REPO_ROOT, "package.json"), "utf8")) as {
|
||||
exports?: Record<string, unknown>;
|
||||
};
|
||||
const exports = packageJson.exports ?? {};
|
||||
const subpaths: string[] = [];
|
||||
for (const key of Object.keys(exports)) {
|
||||
if (key === "./plugin-sdk") {
|
||||
subpaths.push("index");
|
||||
continue;
|
||||
}
|
||||
if (!key.startsWith("./plugin-sdk/")) {
|
||||
continue;
|
||||
}
|
||||
subpaths.push(key.slice("./plugin-sdk/".length));
|
||||
}
|
||||
return subpaths.sort();
|
||||
}
|
||||
|
||||
function collectPluginSdkSourceNames(): string[] {
|
||||
const pluginSdkDir = resolve(REPO_ROOT, "src", "plugin-sdk");
|
||||
return readdirSync(pluginSdkDir, { withFileTypes: true })
|
||||
.filter(
|
||||
(entry) => entry.isFile() && entry.name.endsWith(".ts") && !entry.name.endsWith(".test.ts"),
|
||||
)
|
||||
.map((entry) => entry.name.slice(0, -".ts".length))
|
||||
.sort();
|
||||
}
|
||||
|
||||
function collectTextFiles(rootRelativeDir: string): string[] {
|
||||
const rootDir = resolve(REPO_ROOT, rootRelativeDir);
|
||||
const files: string[] = [];
|
||||
const stack = [rootDir];
|
||||
while (stack.length > 0) {
|
||||
const current = stack.pop();
|
||||
if (!current) {
|
||||
continue;
|
||||
}
|
||||
for (const entry of readdirSync(current, { withFileTypes: true })) {
|
||||
const fullPath = resolve(current, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
if (entry.name === "node_modules" || entry.name === "dist" || entry.name === "coverage") {
|
||||
continue;
|
||||
}
|
||||
stack.push(fullPath);
|
||||
continue;
|
||||
}
|
||||
if (!entry.isFile()) {
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
/\.(?:[cm]?ts|[cm]?js|tsx|jsx|md|mdx|json)$/u.test(entry.name) &&
|
||||
!entry.name.endsWith(".snap")
|
||||
) {
|
||||
files.push(fullPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
function collectPluginSdkSubpathReferences() {
|
||||
const references: Array<{ file: string; subpath: string }> = [];
|
||||
for (const rootRelativeDir of REFERENCE_SCAN_ROOTS) {
|
||||
for (const fullPath of collectTextFiles(rootRelativeDir)) {
|
||||
const source = readFileSync(fullPath, "utf8");
|
||||
for (const match of source.matchAll(PLUGIN_SDK_SUBPATH_PATTERN)) {
|
||||
const subpath = match[1];
|
||||
if (!subpath) {
|
||||
continue;
|
||||
}
|
||||
references.push({
|
||||
file: relative(REPO_ROOT, fullPath).replaceAll("\\", "/"),
|
||||
subpath,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return references;
|
||||
}
|
||||
|
||||
describe("plugin-sdk package contract guardrails", () => {
|
||||
it("keeps package.json exports aligned with built plugin-sdk entrypoints", () => {
|
||||
expect(collectPluginSdkPackageExports()).toEqual([...pluginSdkEntrypoints].sort());
|
||||
});
|
||||
|
||||
it("keeps repo openclaw/plugin-sdk/<name> references on exported built subpaths", () => {
|
||||
const entrypoints = new Set(pluginSdkEntrypoints);
|
||||
const exports = new Set(collectPluginSdkPackageExports());
|
||||
const failures: string[] = [];
|
||||
|
||||
for (const reference of collectPluginSdkSubpathReferences()) {
|
||||
const missingFrom: string[] = [];
|
||||
if (!entrypoints.has(reference.subpath)) {
|
||||
missingFrom.push("scripts/lib/plugin-sdk-entrypoints.json");
|
||||
}
|
||||
if (!exports.has(reference.subpath)) {
|
||||
missingFrom.push("package.json exports");
|
||||
}
|
||||
if (missingFrom.length === 0) {
|
||||
continue;
|
||||
}
|
||||
failures.push(
|
||||
`${reference.file} references openclaw/plugin-sdk/${reference.subpath}, but ${reference.subpath} is missing from ${missingFrom.join(" and ")}`,
|
||||
);
|
||||
}
|
||||
|
||||
expect(failures).toEqual([]);
|
||||
});
|
||||
|
||||
it("does not leave referenced src/plugin-sdk source names stranded outside the public contract", () => {
|
||||
const exported = new Set(pluginSdkEntrypoints);
|
||||
const references = collectPluginSdkSubpathReferences();
|
||||
const failures: string[] = [];
|
||||
|
||||
for (const sourceName of collectPluginSdkSourceNames()) {
|
||||
if (exported.has(sourceName) || sourceName === "compat" || sourceName === "index") {
|
||||
continue;
|
||||
}
|
||||
const matchingRefs = references.filter((reference) => reference.subpath === sourceName);
|
||||
if (matchingRefs.length === 0) {
|
||||
continue;
|
||||
}
|
||||
failures.push(
|
||||
`src/plugin-sdk/${sourceName}.ts is referenced as openclaw/plugin-sdk/${sourceName} in ${matchingRefs
|
||||
.map((reference) => reference.file)
|
||||
.sort()
|
||||
.join(", ")}, but ${sourceName} is not exported as a public plugin-sdk subpath`,
|
||||
);
|
||||
}
|
||||
|
||||
expect(failures).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
|
@ -27,15 +27,25 @@ const RUNTIME_API_EXPORT_GUARDS: Record<string, readonly string[]> = {
|
|||
'export * from "./src/send.js";',
|
||||
],
|
||||
"extensions/imessage/runtime-api.ts": [
|
||||
'export * from "./src/monitor.js";',
|
||||
'export * from "./src/probe.js";',
|
||||
'export * from "./src/send.js";',
|
||||
'export type { IMessageAccountConfig } from "../../src/config/types.imessage.js";',
|
||||
'export type { ChannelPlugin } from "../../src/channels/plugins/types.plugin.js";',
|
||||
'export { DEFAULT_ACCOUNT_ID, PAIRING_APPROVED_MESSAGE, buildChannelConfigSchema, getChatChannelMeta } from "../../src/plugin-sdk/channel-plugin-common.js";',
|
||||
'export { formatTrimmedAllowFromEntries, resolveIMessageConfigAllowFrom, resolveIMessageConfigDefaultTo } from "../../src/plugin-sdk/channel-config-helpers.js";',
|
||||
'export { collectStatusIssuesFromLastError } from "../../src/plugin-sdk/status-helpers.js";',
|
||||
'export { resolveChannelMediaMaxBytes } from "../../src/channels/plugins/media-limits.js";',
|
||||
'export { looksLikeIMessageTargetId, normalizeIMessageMessagingTarget } from "../../src/channels/plugins/normalize/imessage.js";',
|
||||
'export { IMessageConfigSchema } from "../../src/config/zod-schema.providers-core.js";',
|
||||
'export { resolveIMessageGroupRequireMention, resolveIMessageGroupToolPolicy } from "./src/group-policy.js";',
|
||||
'export { monitorIMessageProvider } from "./src/monitor.js";',
|
||||
'export type { MonitorIMessageOpts } from "./src/monitor.js";',
|
||||
'export { probeIMessage } from "./src/probe.js";',
|
||||
'export { sendMessageIMessage } from "./src/send.js";',
|
||||
],
|
||||
"extensions/googlechat/runtime-api.ts": ['export * from "openclaw/plugin-sdk/googlechat";'],
|
||||
"extensions/nextcloud-talk/runtime-api.ts": [
|
||||
'export * from "openclaw/plugin-sdk/nextcloud-talk";',
|
||||
],
|
||||
"extensions/signal/runtime-api.ts": ['export * from "./src/index.js";'],
|
||||
"extensions/signal/runtime-api.ts": ['export * from "./src/runtime-api.js";'],
|
||||
"extensions/slack/runtime-api.ts": [
|
||||
'export * from "./src/action-runtime.js";',
|
||||
'export * from "./src/directory-live.js";',
|
||||
|
|
@ -44,14 +54,21 @@ const RUNTIME_API_EXPORT_GUARDS: Record<string, readonly string[]> = {
|
|||
'export * from "./src/resolve-users.js";',
|
||||
],
|
||||
"extensions/telegram/runtime-api.ts": [
|
||||
'export * from "./src/audit.js";',
|
||||
'export * from "./src/action-runtime.js";',
|
||||
'export * from "./src/channel-actions.js";',
|
||||
'export * from "./src/monitor.js";',
|
||||
'export * from "./src/probe.js";',
|
||||
'export * from "./src/send.js";',
|
||||
'export * from "./src/thread-bindings.js";',
|
||||
'export * from "./src/token.js";',
|
||||
'export type { ChannelPlugin, OpenClawConfig, TelegramActionConfig } from "../../src/plugin-sdk/telegram-core.js";',
|
||||
'export type { ChannelMessageActionAdapter } from "../../src/channels/plugins/types.js";',
|
||||
'export type { TelegramAccountConfig, TelegramNetworkConfig } from "../../src/config/types.js";',
|
||||
'export type { OpenClawPluginApi, OpenClawPluginService, OpenClawPluginServiceContext, PluginLogger } from "../../src/plugins/types.js";',
|
||||
'export type { AcpRuntime, AcpRuntimeCapabilities, AcpRuntimeDoctorReport, AcpRuntimeEnsureInput, AcpRuntimeEvent, AcpRuntimeHandle, AcpRuntimeStatus, AcpRuntimeTurnInput, AcpSessionUpdateTag } from "../../src/acp/runtime/types.js";',
|
||||
'export type { AcpRuntimeErrorCode } from "../../src/acp/runtime/errors.js";',
|
||||
'export { AcpRuntimeError } from "../../src/acp/runtime/errors.js";',
|
||||
'export { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../../src/routing/session-key.js";',
|
||||
'export { buildChannelConfigSchema, getChatChannelMeta, jsonResult, readNumberParam, readReactionParams, readStringArrayParam, readStringOrNumberParam, readStringParam, resolvePollMaxSelections, TelegramConfigSchema } from "../../src/plugin-sdk/telegram-core.js";',
|
||||
'export { parseTelegramTopicConversation } from "../../src/acp/conversation-id.js";',
|
||||
'export { clearAccountEntryFields } from "../../src/channels/plugins/config-helpers.js";',
|
||||
'export { buildTokenChannelStatusSummary } from "../../src/plugin-sdk/status-helpers.js";',
|
||||
'export { projectCredentialSnapshotFields, resolveConfiguredFromCredentialStatuses } from "../../src/channels/account-snapshot-fields.js";',
|
||||
'export { resolveTelegramPollVisibility } from "../../src/poll-params.js";',
|
||||
'export { PAIRING_APPROVED_MESSAGE } from "../../src/channels/plugins/pairing-message.js";',
|
||||
],
|
||||
"extensions/whatsapp/runtime-api.ts": [
|
||||
'export * from "./src/active-listener.js";',
|
||||
|
|
|
|||
Loading…
Reference in New Issue