mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-21 15:01:03 +00:00
* 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>
168 lines
8.4 KiB
TypeScript
168 lines
8.4 KiB
TypeScript
import { readdirSync, readFileSync } from "node:fs";
|
|
import { dirname, relative, resolve } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import ts from "typescript";
|
|
import { describe, expect, it } from "vitest";
|
|
|
|
const ROOT_DIR = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
|
|
const RUNTIME_API_EXPORT_GUARDS: Record<string, readonly string[]> = {
|
|
"extensions/discord/runtime-api.ts": [
|
|
'export * from "./src/audit.js";',
|
|
'export * from "./src/actions/runtime.js";',
|
|
'export * from "./src/actions/runtime.moderation-shared.js";',
|
|
'export * from "./src/actions/runtime.shared.js";',
|
|
'export * from "./src/channel-actions.js";',
|
|
'export * from "./src/directory-live.js";',
|
|
'export * from "./src/monitor.js";',
|
|
'export * from "./src/monitor/gateway-plugin.js";',
|
|
'export * from "./src/monitor/gateway-registry.js";',
|
|
'export * from "./src/monitor/presence-cache.js";',
|
|
'export * from "./src/monitor/thread-bindings.js";',
|
|
'export * from "./src/monitor/thread-bindings.manager.js";',
|
|
'export * from "./src/monitor/timeouts.js";',
|
|
'export * from "./src/probe.js";',
|
|
'export * from "./src/resolve-channels.js";',
|
|
'export * from "./src/resolve-users.js";',
|
|
'export * from "./src/send.js";',
|
|
],
|
|
"extensions/imessage/runtime-api.ts": [
|
|
'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/runtime-api.js";'],
|
|
"extensions/slack/runtime-api.ts": [
|
|
'export * from "./src/action-runtime.js";',
|
|
'export * from "./src/directory-live.js";',
|
|
'export * from "./src/index.js";',
|
|
'export * from "./src/resolve-channels.js";',
|
|
'export * from "./src/resolve-users.js";',
|
|
],
|
|
"extensions/telegram/runtime-api.ts": [
|
|
'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";',
|
|
'export * from "./src/action-runtime.js";',
|
|
'export * from "./src/agent-tools-login.js";',
|
|
'export * from "./src/auth-store.js";',
|
|
'export * from "./src/auto-reply.js";',
|
|
'export * from "./src/inbound.js";',
|
|
'export * from "./src/login.js";',
|
|
'export * from "./src/media.js";',
|
|
'export * from "./src/send.js";',
|
|
'export * from "./src/session.js";',
|
|
],
|
|
} as const;
|
|
|
|
function collectRuntimeApiFiles(): string[] {
|
|
const extensionsDir = resolve(ROOT_DIR, "..", "extensions");
|
|
const files: string[] = [];
|
|
const stack = [extensionsDir];
|
|
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() || entry.name !== "runtime-api.ts") {
|
|
continue;
|
|
}
|
|
files.push(relative(resolve(ROOT_DIR, ".."), fullPath).replaceAll("\\", "/"));
|
|
}
|
|
}
|
|
return files;
|
|
}
|
|
|
|
function readExportStatements(path: string): string[] {
|
|
const sourceText = readFileSync(resolve(ROOT_DIR, "..", path), "utf8");
|
|
const sourceFile = ts.createSourceFile(path, sourceText, ts.ScriptTarget.Latest, true);
|
|
|
|
return sourceFile.statements.flatMap((statement) => {
|
|
if (!ts.isExportDeclaration(statement)) {
|
|
const modifiers = ts.canHaveModifiers(statement) ? ts.getModifiers(statement) : undefined;
|
|
if (!modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword)) {
|
|
return [];
|
|
}
|
|
return [statement.getText(sourceFile).replaceAll(/\s+/g, " ").trim()];
|
|
}
|
|
|
|
const moduleSpecifier = statement.moduleSpecifier;
|
|
if (!moduleSpecifier || !ts.isStringLiteral(moduleSpecifier)) {
|
|
return [statement.getText(sourceFile).replaceAll(/\s+/g, " ").trim()];
|
|
}
|
|
|
|
if (!statement.exportClause) {
|
|
const prefix = statement.isTypeOnly ? "export type *" : "export *";
|
|
return [`${prefix} from ${moduleSpecifier.getText(sourceFile)};`];
|
|
}
|
|
|
|
if (!ts.isNamedExports(statement.exportClause)) {
|
|
return [statement.getText(sourceFile).replaceAll(/\s+/g, " ").trim()];
|
|
}
|
|
|
|
const specifiers = statement.exportClause.elements.map((element) => {
|
|
const imported = element.propertyName?.text;
|
|
const exported = element.name.text;
|
|
const alias = imported ? `${imported} as ${exported}` : exported;
|
|
return element.isTypeOnly ? `type ${alias}` : alias;
|
|
});
|
|
const exportPrefix = statement.isTypeOnly ? "export type" : "export";
|
|
return [
|
|
`${exportPrefix} { ${specifiers.join(", ")} } from ${moduleSpecifier.getText(sourceFile)};`,
|
|
];
|
|
});
|
|
}
|
|
|
|
describe("runtime api guardrails", () => {
|
|
it("keeps runtime api seams on an explicit export allowlist", () => {
|
|
const runtimeApiFiles = collectRuntimeApiFiles();
|
|
expect(runtimeApiFiles).toEqual(
|
|
expect.arrayContaining(Object.keys(RUNTIME_API_EXPORT_GUARDS).toSorted()),
|
|
);
|
|
|
|
for (const file of Object.keys(RUNTIME_API_EXPORT_GUARDS).toSorted()) {
|
|
expect(readExportStatements(file), `${file} runtime api exports changed`).toEqual(
|
|
RUNTIME_API_EXPORT_GUARDS[file],
|
|
);
|
|
}
|
|
});
|
|
});
|