mirror of https://github.com/openclaw/openclaw.git
refactor: harden plugin metadata and bundled channel entry seams
This commit is contained in:
parent
95079949c3
commit
8cb85ff85f
|
|
@ -1,5 +1,5 @@
|
|||
import type { IncomingMessage, ServerResponse } from "node:http";
|
||||
import { safeEqualSecret } from "openclaw/plugin-sdk/browser-support";
|
||||
import { safeEqualSecret } from "openclaw/plugin-sdk/browser-security-runtime";
|
||||
import { isPrivateNetworkOptInEnabled } from "openclaw/plugin-sdk/ssrf-runtime";
|
||||
import { createBlueBubblesDebounceRegistry } from "./monitor-debounce.js";
|
||||
import { normalizeWebhookMessage, normalizeWebhookReaction } from "./monitor-normalize.js";
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
// Keep bundled channel entry imports narrow so bootstrap/discovery paths do
|
||||
// not drag the broad Discord API barrel into lightweight plugin loads.
|
||||
export { discordPlugin } from "./src/channel.js";
|
||||
export { discordSetupPlugin } from "./src/channel.setup.js";
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import entry from "./index.js";
|
||||
import setupEntry from "./setup-entry.js";
|
||||
|
||||
describe("discord bundled entries", () => {
|
||||
it("loads the channel plugin without importing the broad api barrel", () => {
|
||||
const plugin = entry.loadChannelPlugin();
|
||||
expect(plugin.id).toBe("discord");
|
||||
});
|
||||
|
||||
it("loads the setup plugin without importing the broad api barrel", () => {
|
||||
const plugin = setupEntry.loadSetupPlugin();
|
||||
expect(plugin.id).toBe("discord");
|
||||
});
|
||||
});
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
import { defineBundledChannelEntry } from "openclaw/plugin-sdk/channel-entry-contract";
|
||||
|
||||
type DiscordSubagentHooksModule = typeof import("./api.js");
|
||||
type DiscordSubagentHooksModule = typeof import("./subagent-hooks-api.js");
|
||||
|
||||
let discordSubagentHooksPromise: Promise<DiscordSubagentHooksModule> | null = null;
|
||||
|
||||
function loadDiscordSubagentHooksModule() {
|
||||
discordSubagentHooksPromise ??= import("./api.js");
|
||||
discordSubagentHooksPromise ??= import("./subagent-hooks-api.js");
|
||||
return discordSubagentHooksPromise;
|
||||
}
|
||||
|
||||
|
|
@ -15,7 +15,7 @@ export default defineBundledChannelEntry({
|
|||
description: "Discord channel plugin",
|
||||
importMetaUrl: import.meta.url,
|
||||
plugin: {
|
||||
specifier: "./api.js",
|
||||
specifier: "./channel-plugin-api.js",
|
||||
exportName: "discordPlugin",
|
||||
},
|
||||
runtime: {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { defineBundledChannelSetupEntry } from "openclaw/plugin-sdk/channel-entr
|
|||
export default defineBundledChannelSetupEntry({
|
||||
importMetaUrl: import.meta.url,
|
||||
plugin: {
|
||||
specifier: "./api.js",
|
||||
specifier: "./channel-plugin-api.js",
|
||||
exportName: "discordSetupPlugin",
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
// Subagent hooks live behind a dedicated barrel so the bundled entry can lazy
|
||||
// load only the handlers it needs.
|
||||
export {
|
||||
handleDiscordSubagentDeliveryTarget,
|
||||
handleDiscordSubagentEnded,
|
||||
handleDiscordSubagentSpawning,
|
||||
} from "./src/subagent-hooks.js";
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
export type { RuntimeEnv } from "../runtime-api.js";
|
||||
export { safeEqualSecret } from "openclaw/plugin-sdk/browser-support";
|
||||
export { safeEqualSecret } from "openclaw/plugin-sdk/browser-security-runtime";
|
||||
export {
|
||||
applyBasicWebhookRequestGuards,
|
||||
isRequestBodyLimitError,
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ export { evaluateSenderGroupAccessForPolicy } from "openclaw/plugin-sdk/group-ac
|
|||
export { createChannelReplyPipeline } from "openclaw/plugin-sdk/channel-reply-pipeline";
|
||||
export { logTypingFailure } from "openclaw/plugin-sdk/channel-feedback";
|
||||
export { loadOutboundMediaFromUrl } from "openclaw/plugin-sdk/outbound-media";
|
||||
export { rawDataToString } from "openclaw/plugin-sdk/browser-support";
|
||||
export { rawDataToString } from "openclaw/plugin-sdk/browser-node-runtime";
|
||||
export { chunkTextForOutbound } from "openclaw/plugin-sdk/text-chunking";
|
||||
export {
|
||||
DEFAULT_GROUP_HISTORY_LIMIT,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { createHmac } from "node:crypto";
|
||||
import type { IncomingMessage, ServerResponse } from "node:http";
|
||||
import { safeEqualSecret } from "openclaw/plugin-sdk/browser-support";
|
||||
import { safeEqualSecret } from "openclaw/plugin-sdk/browser-security-runtime";
|
||||
import { getMattermostRuntime } from "../runtime.js";
|
||||
import { updateMattermostPost, type MattermostClient, type MattermostPost } from "./client.js";
|
||||
import { isTrustedProxyAddress, resolveClientIp, type OpenClawConfig } from "./runtime-api.js";
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import type { IncomingMessage, ServerResponse } from "node:http";
|
||||
import { safeEqualSecret } from "openclaw/plugin-sdk/browser-support";
|
||||
import { safeEqualSecret } from "openclaw/plugin-sdk/browser-security-runtime";
|
||||
import { isPrivateNetworkOptInEnabled } from "openclaw/plugin-sdk/ssrf-runtime";
|
||||
import type { ResolvedMattermostAccount } from "../mattermost/accounts.js";
|
||||
import { getMattermostRuntime } from "../runtime.js";
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
* Security module: token validation, rate limiting, input sanitization, user allowlist.
|
||||
*/
|
||||
|
||||
import { safeEqualSecret } from "openclaw/plugin-sdk/browser-support";
|
||||
import { safeEqualSecret } from "openclaw/plugin-sdk/browser-security-runtime";
|
||||
import {
|
||||
createFixedWindowRateLimiter,
|
||||
type FixedWindowRateLimiter,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { createServer } from "node:http";
|
|||
import type { IncomingMessage } from "node:http";
|
||||
import net from "node:net";
|
||||
import * as grammy from "grammy";
|
||||
import { safeEqualSecret } from "openclaw/plugin-sdk/browser-support";
|
||||
import { safeEqualSecret } from "openclaw/plugin-sdk/browser-security-runtime";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
|
||||
import { isDiagnosticsEnabled } from "openclaw/plugin-sdk/diagnostic-runtime";
|
||||
import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env";
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import crypto from "node:crypto";
|
||||
import { safeEqualSecret } from "openclaw/plugin-sdk/browser-support";
|
||||
import { safeEqualSecret } from "openclaw/plugin-sdk/browser-security-runtime";
|
||||
import type { TwilioConfig, WebhookSecurityConfig } from "../config.js";
|
||||
import { getHeader } from "../http-headers.js";
|
||||
import type { MediaStreamHandler } from "../media-stream.js";
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import crypto from "node:crypto";
|
||||
import { safeEqualSecret } from "openclaw/plugin-sdk/browser-support";
|
||||
import { safeEqualSecret } from "openclaw/plugin-sdk/browser-security-runtime";
|
||||
import { getHeader } from "./http-headers.js";
|
||||
import type { WebhookContext } from "./types.js";
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
// Keep bundled channel bootstrap loads narrow so lightweight config-presence
|
||||
// probes do not import the broad WhatsApp API barrel.
|
||||
export { whatsappPlugin } from "./src/channel.js";
|
||||
export { whatsappSetupPlugin } from "./src/channel.setup.js";
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import entry from "./index.js";
|
||||
import setupEntry from "./setup-entry.js";
|
||||
|
||||
describe("whatsapp bundled entries", () => {
|
||||
it("loads the channel plugin without importing the broad api barrel", () => {
|
||||
const plugin = entry.loadChannelPlugin();
|
||||
expect(plugin.id).toBe("whatsapp");
|
||||
});
|
||||
|
||||
it("loads the setup plugin without importing the broad api barrel", () => {
|
||||
const plugin = setupEntry.loadSetupPlugin();
|
||||
expect(plugin.id).toBe("whatsapp");
|
||||
});
|
||||
});
|
||||
|
|
@ -6,7 +6,7 @@ export default defineBundledChannelEntry({
|
|||
description: "WhatsApp channel plugin",
|
||||
importMetaUrl: import.meta.url,
|
||||
plugin: {
|
||||
specifier: "./api.js",
|
||||
specifier: "./channel-plugin-api.js",
|
||||
exportName: "whatsappPlugin",
|
||||
},
|
||||
runtime: {
|
||||
|
|
|
|||
|
|
@ -31,7 +31,11 @@
|
|||
"docsPath": "/channels/whatsapp",
|
||||
"docsLabel": "whatsapp",
|
||||
"blurb": "works with your own number; recommend a separate phone + eSIM.",
|
||||
"systemImage": "message"
|
||||
"systemImage": "message",
|
||||
"persistedAuthState": {
|
||||
"specifier": "./auth-presence",
|
||||
"exportName": "hasAnyWhatsAppAuth"
|
||||
}
|
||||
},
|
||||
"install": {
|
||||
"npmSpec": "@openclaw/whatsapp",
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { defineBundledChannelSetupEntry } from "openclaw/plugin-sdk/channel-entr
|
|||
export default defineBundledChannelSetupEntry({
|
||||
importMetaUrl: import.meta.url,
|
||||
plugin: {
|
||||
specifier: "./api.js",
|
||||
specifier: "./channel-plugin-api.js",
|
||||
exportName: "whatsappSetupPlugin",
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,9 +1,36 @@
|
|||
import { execFile } from "node:child_process";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { promisify } from "node:util";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
|
||||
const zaloRuntimeImportEnv = {
|
||||
HOME: process.env.HOME,
|
||||
NODE_OPTIONS: process.env.NODE_OPTIONS,
|
||||
NODE_PATH: process.env.NODE_PATH,
|
||||
PATH: process.env.PATH,
|
||||
TERM: process.env.TERM,
|
||||
} satisfies NodeJS.ProcessEnv;
|
||||
|
||||
describe("zalo runtime api", () => {
|
||||
it("exports the channel plugin without reentering setup surfaces", async () => {
|
||||
const runtimeApi = await import("./runtime-api.js");
|
||||
const { stdout } = await execFileAsync(
|
||||
process.execPath,
|
||||
[
|
||||
"--import",
|
||||
"tsx",
|
||||
"-e",
|
||||
'const runtimeApi = await import("./extensions/zalo/runtime-api.ts"); process.stdout.write(runtimeApi.zaloPlugin.id);',
|
||||
],
|
||||
{
|
||||
cwd: repoRoot,
|
||||
env: zaloRuntimeImportEnv,
|
||||
timeout: 40_000,
|
||||
},
|
||||
);
|
||||
|
||||
expect(runtimeApi.zaloPlugin.id).toBe("zalo");
|
||||
});
|
||||
expect(stdout).toBe("zalo");
|
||||
}, 45_000);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,94 +2,4 @@
|
|||
// Keep this barrel thin and free of local plugin self-imports so the bundled
|
||||
// entry loader can resolve the channel plugin without re-entering this module.
|
||||
export { zaloPlugin } from "./src/channel.js";
|
||||
export type { ReplyPayload } from "openclaw/plugin-sdk/reply-runtime";
|
||||
export type { OpenClawConfig, GroupPolicy } from "openclaw/plugin-sdk/config-runtime";
|
||||
export type { MarkdownTableMode } from "openclaw/plugin-sdk/config-runtime";
|
||||
export type { BaseTokenResolution } from "openclaw/plugin-sdk/channel-contract";
|
||||
export type {
|
||||
BaseProbeResult,
|
||||
ChannelAccountSnapshot,
|
||||
ChannelMessageActionAdapter,
|
||||
ChannelMessageActionName,
|
||||
ChannelStatusIssue,
|
||||
} from "openclaw/plugin-sdk/channel-contract";
|
||||
export type { SecretInput } from "openclaw/plugin-sdk/secret-input";
|
||||
export type { SenderGroupAccessDecision } from "openclaw/plugin-sdk/group-access";
|
||||
export type { ChannelPlugin, PluginRuntime, WizardPrompter } from "openclaw/plugin-sdk/core";
|
||||
export type { RuntimeEnv } from "openclaw/plugin-sdk/runtime";
|
||||
export type { OutboundReplyPayload } from "openclaw/plugin-sdk/reply-payload";
|
||||
export {
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
buildChannelConfigSchema,
|
||||
createDedupeCache,
|
||||
formatPairingApproveHint,
|
||||
jsonResult,
|
||||
normalizeAccountId,
|
||||
readStringParam,
|
||||
resolveClientIp,
|
||||
} from "openclaw/plugin-sdk/core";
|
||||
export {
|
||||
applyAccountNameToChannelSection,
|
||||
applySetupAccountConfigPatch,
|
||||
buildSingleChannelSecretPromptState,
|
||||
mergeAllowFromEntries,
|
||||
migrateBaseNameToDefaultAccount,
|
||||
promptSingleChannelSecretInput,
|
||||
runSingleChannelSecretStep,
|
||||
setTopLevelChannelDmPolicyWithAllowFrom,
|
||||
} from "openclaw/plugin-sdk/setup";
|
||||
export {
|
||||
buildSecretInputSchema,
|
||||
hasConfiguredSecretInput,
|
||||
normalizeResolvedSecretInputString,
|
||||
normalizeSecretInputString,
|
||||
} from "openclaw/plugin-sdk/secret-input";
|
||||
export {
|
||||
buildTokenChannelStatusSummary,
|
||||
PAIRING_APPROVED_MESSAGE,
|
||||
} from "openclaw/plugin-sdk/channel-status";
|
||||
export { buildBaseAccountStatusSnapshot } from "openclaw/plugin-sdk/status-helpers";
|
||||
export { chunkTextForOutbound } from "openclaw/plugin-sdk/text-chunking";
|
||||
export {
|
||||
formatAllowFromLowercase,
|
||||
isNormalizedSenderAllowed,
|
||||
} from "openclaw/plugin-sdk/allow-from";
|
||||
export { addWildcardAllowFrom } from "openclaw/plugin-sdk/setup";
|
||||
export { evaluateSenderGroupAccess } from "openclaw/plugin-sdk/group-access";
|
||||
export { resolveOpenProviderRuntimeGroupPolicy } from "openclaw/plugin-sdk/config-runtime";
|
||||
export {
|
||||
warnMissingProviderGroupPolicyFallbackOnce,
|
||||
resolveDefaultGroupPolicy,
|
||||
} from "openclaw/plugin-sdk/config-runtime";
|
||||
export { createChannelPairingController } from "openclaw/plugin-sdk/channel-pairing";
|
||||
export { createChannelReplyPipeline } from "openclaw/plugin-sdk/channel-reply-pipeline";
|
||||
export { logTypingFailure } from "openclaw/plugin-sdk/channel-feedback";
|
||||
export {
|
||||
deliverTextOrMediaReply,
|
||||
isNumericTargetId,
|
||||
sendPayloadWithChunkedTextAndMedia,
|
||||
} from "openclaw/plugin-sdk/reply-payload";
|
||||
export {
|
||||
resolveDirectDmAuthorizationOutcome,
|
||||
resolveSenderCommandAuthorizationWithRuntime,
|
||||
} from "openclaw/plugin-sdk/command-auth";
|
||||
export { resolveInboundRouteEnvelopeBuilderWithRuntime } from "openclaw/plugin-sdk/inbound-envelope";
|
||||
export { waitForAbortSignal } from "openclaw/plugin-sdk/runtime";
|
||||
export {
|
||||
applyBasicWebhookRequestGuards,
|
||||
createFixedWindowRateLimiter,
|
||||
createWebhookAnomalyTracker,
|
||||
readJsonWebhookBodyOrReject,
|
||||
registerWebhookTarget,
|
||||
registerWebhookTargetWithPluginRoute,
|
||||
resolveWebhookPath,
|
||||
resolveWebhookTargetWithAuthOrRejectSync,
|
||||
WEBHOOK_ANOMALY_COUNTER_DEFAULTS,
|
||||
WEBHOOK_RATE_LIMIT_DEFAULTS,
|
||||
withResolvedWebhookRequestPipeline,
|
||||
} from "openclaw/plugin-sdk/webhook-ingress";
|
||||
export type {
|
||||
RegisterWebhookPluginRouteOptions,
|
||||
RegisterWebhookTargetOptions,
|
||||
} from "openclaw/plugin-sdk/webhook-ingress";
|
||||
export { setZaloRuntime } from "./src/runtime.js";
|
||||
export * from "./src/runtime-api.js";
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { IncomingMessage, ServerResponse } from "node:http";
|
||||
import { safeEqualSecret } from "openclaw/plugin-sdk/browser-support";
|
||||
import { safeEqualSecret } from "openclaw/plugin-sdk/browser-security-runtime";
|
||||
import type { ResolvedZaloAccount } from "./accounts.js";
|
||||
import type { ZaloFetch, ZaloUpdate } from "./api.js";
|
||||
import type { ZaloRuntimeEnv } from "./monitor.js";
|
||||
|
|
|
|||
|
|
@ -1 +1,5 @@
|
|||
export * from "../runtime-api.js";
|
||||
// Internal runtime barrel. Keep this independent from the public top-level
|
||||
// runtime barrel so local imports do not loop back through the plugin export
|
||||
// surface during entry loading.
|
||||
export * from "./runtime-support.js";
|
||||
export { setZaloRuntime } from "./runtime.js";
|
||||
|
|
|
|||
|
|
@ -0,0 +1,90 @@
|
|||
export type { ReplyPayload } from "openclaw/plugin-sdk/reply-runtime";
|
||||
export type { OpenClawConfig, GroupPolicy } from "openclaw/plugin-sdk/config-runtime";
|
||||
export type { MarkdownTableMode } from "openclaw/plugin-sdk/config-runtime";
|
||||
export type { BaseTokenResolution } from "openclaw/plugin-sdk/channel-contract";
|
||||
export type {
|
||||
BaseProbeResult,
|
||||
ChannelAccountSnapshot,
|
||||
ChannelMessageActionAdapter,
|
||||
ChannelMessageActionName,
|
||||
ChannelStatusIssue,
|
||||
} from "openclaw/plugin-sdk/channel-contract";
|
||||
export type { SecretInput } from "openclaw/plugin-sdk/secret-input";
|
||||
export type { SenderGroupAccessDecision } from "openclaw/plugin-sdk/group-access";
|
||||
export type { ChannelPlugin, PluginRuntime, WizardPrompter } from "openclaw/plugin-sdk/core";
|
||||
export type { RuntimeEnv } from "openclaw/plugin-sdk/runtime";
|
||||
export type { OutboundReplyPayload } from "openclaw/plugin-sdk/reply-payload";
|
||||
export {
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
buildChannelConfigSchema,
|
||||
createDedupeCache,
|
||||
formatPairingApproveHint,
|
||||
jsonResult,
|
||||
normalizeAccountId,
|
||||
readStringParam,
|
||||
resolveClientIp,
|
||||
} from "openclaw/plugin-sdk/core";
|
||||
export {
|
||||
applyAccountNameToChannelSection,
|
||||
applySetupAccountConfigPatch,
|
||||
buildSingleChannelSecretPromptState,
|
||||
mergeAllowFromEntries,
|
||||
migrateBaseNameToDefaultAccount,
|
||||
promptSingleChannelSecretInput,
|
||||
runSingleChannelSecretStep,
|
||||
setTopLevelChannelDmPolicyWithAllowFrom,
|
||||
} from "openclaw/plugin-sdk/setup";
|
||||
export {
|
||||
buildSecretInputSchema,
|
||||
hasConfiguredSecretInput,
|
||||
normalizeResolvedSecretInputString,
|
||||
normalizeSecretInputString,
|
||||
} from "openclaw/plugin-sdk/secret-input";
|
||||
export {
|
||||
buildTokenChannelStatusSummary,
|
||||
PAIRING_APPROVED_MESSAGE,
|
||||
} from "openclaw/plugin-sdk/channel-status";
|
||||
export { buildBaseAccountStatusSnapshot } from "openclaw/plugin-sdk/status-helpers";
|
||||
export { chunkTextForOutbound } from "openclaw/plugin-sdk/text-chunking";
|
||||
export {
|
||||
formatAllowFromLowercase,
|
||||
isNormalizedSenderAllowed,
|
||||
} from "openclaw/plugin-sdk/allow-from";
|
||||
export { addWildcardAllowFrom } from "openclaw/plugin-sdk/setup";
|
||||
export { evaluateSenderGroupAccess } from "openclaw/plugin-sdk/group-access";
|
||||
export { resolveOpenProviderRuntimeGroupPolicy } from "openclaw/plugin-sdk/config-runtime";
|
||||
export {
|
||||
warnMissingProviderGroupPolicyFallbackOnce,
|
||||
resolveDefaultGroupPolicy,
|
||||
} from "openclaw/plugin-sdk/config-runtime";
|
||||
export { createChannelPairingController } from "openclaw/plugin-sdk/channel-pairing";
|
||||
export { createChannelReplyPipeline } from "openclaw/plugin-sdk/channel-reply-pipeline";
|
||||
export { logTypingFailure } from "openclaw/plugin-sdk/channel-feedback";
|
||||
export {
|
||||
deliverTextOrMediaReply,
|
||||
isNumericTargetId,
|
||||
sendPayloadWithChunkedTextAndMedia,
|
||||
} from "openclaw/plugin-sdk/reply-payload";
|
||||
export {
|
||||
resolveDirectDmAuthorizationOutcome,
|
||||
resolveSenderCommandAuthorizationWithRuntime,
|
||||
} from "openclaw/plugin-sdk/command-auth";
|
||||
export { resolveInboundRouteEnvelopeBuilderWithRuntime } from "openclaw/plugin-sdk/inbound-envelope";
|
||||
export { waitForAbortSignal } from "openclaw/plugin-sdk/runtime";
|
||||
export {
|
||||
applyBasicWebhookRequestGuards,
|
||||
createFixedWindowRateLimiter,
|
||||
createWebhookAnomalyTracker,
|
||||
readJsonWebhookBodyOrReject,
|
||||
registerWebhookTarget,
|
||||
registerWebhookTargetWithPluginRoute,
|
||||
resolveWebhookPath,
|
||||
resolveWebhookTargetWithAuthOrRejectSync,
|
||||
WEBHOOK_ANOMALY_COUNTER_DEFAULTS,
|
||||
WEBHOOK_RATE_LIMIT_DEFAULTS,
|
||||
withResolvedWebhookRequestPipeline,
|
||||
} from "openclaw/plugin-sdk/webhook-ingress";
|
||||
export type {
|
||||
RegisterWebhookPluginRouteOptions,
|
||||
RegisterWebhookTargetOptions,
|
||||
} from "openclaw/plugin-sdk/webhook-ingress";
|
||||
|
|
@ -58,4 +58,4 @@ export {
|
|||
sendPayloadWithChunkedTextAndMedia,
|
||||
type OutboundReplyPayload,
|
||||
} from "openclaw/plugin-sdk/reply-payload";
|
||||
export { resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk/browser-support";
|
||||
export { resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk/browser-security-runtime";
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import {
|
||||
hasBundledChannelPersistedAuthState,
|
||||
listBundledChannelIdsWithPersistedAuthState,
|
||||
} from "../channels/plugins/persisted-auth-state.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { resolveStateDir } from "../config/paths.js";
|
||||
import { getBootstrapChannelPlugin } from "./plugins/bootstrap-registry.js";
|
||||
import { listBundledChannelPluginIds } from "./plugins/bundled-ids.js";
|
||||
|
||||
const IGNORED_CHANNEL_CONFIG_KEYS = new Set(["defaults", "modelByChannel"]);
|
||||
|
|
@ -39,6 +42,8 @@ function hasPersistedChannelState(env: NodeJS.ProcessEnv): boolean {
|
|||
return fs.existsSync(resolveStateDir(env, os.homedir));
|
||||
}
|
||||
|
||||
const PERSISTED_AUTH_STATE_CHANNEL_IDS = listBundledChannelIdsWithPersistedAuthState();
|
||||
|
||||
export function listPotentialConfiguredChannelIds(
|
||||
cfg: OpenClawConfig,
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
|
|
@ -71,9 +76,8 @@ export function listPotentialConfiguredChannelIds(
|
|||
}
|
||||
|
||||
if (options.includePersistedAuthState !== false && hasPersistedChannelState(env)) {
|
||||
for (const channelId of channelIds) {
|
||||
const plugin = getBootstrapChannelPlugin(channelId);
|
||||
if (plugin?.config?.hasPersistedAuthState?.({ cfg, env })) {
|
||||
for (const channelId of PERSISTED_AUTH_STATE_CHANNEL_IDS) {
|
||||
if (hasBundledChannelPersistedAuthState({ channelId, cfg, env })) {
|
||||
configuredChannelIds.add(channelId);
|
||||
}
|
||||
}
|
||||
|
|
@ -100,8 +104,8 @@ function hasEnvConfiguredChannel(
|
|||
if (options.includePersistedAuthState === false || !hasPersistedChannelState(env)) {
|
||||
return false;
|
||||
}
|
||||
return channelIds.some((channelId) =>
|
||||
Boolean(getBootstrapChannelPlugin(channelId)?.config?.hasPersistedAuthState?.({ cfg, env })),
|
||||
return PERSISTED_AUTH_STATE_CHANNEL_IDS.some((channelId) =>
|
||||
hasBundledChannelPersistedAuthState({ channelId, cfg, env }),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -61,11 +61,21 @@ function getBootstrapPlugins(): CachedBootstrapPlugins {
|
|||
return cachedBootstrapPlugins;
|
||||
}
|
||||
|
||||
export function listBootstrapChannelPlugins(): readonly ChannelPlugin[] {
|
||||
return getBootstrapPlugins().sortedIds.flatMap((id) => {
|
||||
export function listBootstrapChannelPluginIds(): readonly string[] {
|
||||
return getBootstrapPlugins().sortedIds;
|
||||
}
|
||||
|
||||
export function* iterateBootstrapChannelPlugins(): IterableIterator<ChannelPlugin> {
|
||||
for (const id of listBootstrapChannelPluginIds()) {
|
||||
const plugin = getBootstrapChannelPlugin(id);
|
||||
return plugin ? [plugin] : [];
|
||||
});
|
||||
if (plugin) {
|
||||
yield plugin;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function listBootstrapChannelPlugins(): readonly ChannelPlugin[] {
|
||||
return [...iterateBootstrapChannelPlugins()];
|
||||
}
|
||||
|
||||
export function getBootstrapChannelPlugin(id: ChannelId): ChannelPlugin | undefined {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
import type { LegacyConfigRule } from "../../config/legacy.shared.js";
|
||||
import { listBootstrapChannelPlugins } from "./bootstrap-registry.js";
|
||||
import { iterateBootstrapChannelPlugins } from "./bootstrap-registry.js";
|
||||
|
||||
export function collectChannelLegacyConfigRules(): LegacyConfigRule[] {
|
||||
return listBootstrapChannelPlugins().flatMap((plugin) => plugin.doctor?.legacyConfigRules ?? []);
|
||||
const rules: LegacyConfigRule[] = [];
|
||||
for (const plugin of iterateBootstrapChannelPlugins()) {
|
||||
rules.push(...(plugin.doctor?.legacyConfigRules ?? []));
|
||||
}
|
||||
return rules;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,178 @@
|
|||
import fs from "node:fs";
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
import { createJiti } from "jiti";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import { openBoundaryFileSync } from "../../infra/boundary-file-read.js";
|
||||
import { createSubsystemLogger } from "../../logging/subsystem.js";
|
||||
import {
|
||||
listChannelCatalogEntries,
|
||||
type PluginChannelCatalogEntry,
|
||||
} from "../../plugins/channel-catalog-registry.js";
|
||||
import {
|
||||
buildPluginLoaderAliasMap,
|
||||
buildPluginLoaderJitiOptions,
|
||||
shouldPreferNativeJiti,
|
||||
} from "../../plugins/sdk-alias.js";
|
||||
|
||||
type PersistedAuthStateChecker = (params: {
|
||||
cfg: OpenClawConfig;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}) => boolean;
|
||||
|
||||
type PersistedAuthStateMetadata = {
|
||||
specifier?: string;
|
||||
exportName?: string;
|
||||
};
|
||||
|
||||
const log = createSubsystemLogger("channels");
|
||||
const nodeRequire = createRequire(import.meta.url);
|
||||
const persistedAuthStateCatalog = listChannelCatalogEntries({ origin: "bundled" }).filter((entry) =>
|
||||
Boolean(resolvePersistedAuthStateMetadata(entry)),
|
||||
);
|
||||
const persistedAuthStateEntriesById = new Map(
|
||||
persistedAuthStateCatalog.map((entry) => [entry.pluginId, entry] as const),
|
||||
);
|
||||
const persistedAuthStateCheckerCache = new Map<string, PersistedAuthStateChecker | null>();
|
||||
|
||||
function resolvePersistedAuthStateMetadata(
|
||||
entry: PluginChannelCatalogEntry,
|
||||
): PersistedAuthStateMetadata | null {
|
||||
const metadata = entry.channel.persistedAuthState;
|
||||
if (!metadata || typeof metadata !== "object") {
|
||||
return null;
|
||||
}
|
||||
const specifier = typeof metadata.specifier === "string" ? metadata.specifier.trim() : "";
|
||||
const exportName = typeof metadata.exportName === "string" ? metadata.exportName.trim() : "";
|
||||
if (!specifier || !exportName) {
|
||||
return null;
|
||||
}
|
||||
return { specifier, exportName };
|
||||
}
|
||||
|
||||
function createModuleLoader() {
|
||||
const jitiLoaders = new Map<string, ReturnType<typeof createJiti>>();
|
||||
|
||||
return (modulePath: string) => {
|
||||
const tryNative =
|
||||
shouldPreferNativeJiti(modulePath) || modulePath.includes(`${path.sep}dist${path.sep}`);
|
||||
const aliasMap = buildPluginLoaderAliasMap(modulePath, process.argv[1], import.meta.url);
|
||||
const cacheKey = JSON.stringify({
|
||||
tryNative,
|
||||
aliasMap: Object.entries(aliasMap).toSorted(([left], [right]) => left.localeCompare(right)),
|
||||
});
|
||||
const cached = jitiLoaders.get(cacheKey);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
const loader = createJiti(import.meta.url, {
|
||||
...buildPluginLoaderJitiOptions(aliasMap),
|
||||
tryNative,
|
||||
});
|
||||
jitiLoaders.set(cacheKey, loader);
|
||||
return loader;
|
||||
};
|
||||
}
|
||||
|
||||
const loadModule = createModuleLoader();
|
||||
|
||||
function resolveModuleCandidates(entry: PluginChannelCatalogEntry, specifier: string): string[] {
|
||||
const normalizedSpecifier = specifier.replace(/\\/g, "/");
|
||||
const resolvedPath = path.resolve(entry.rootDir, normalizedSpecifier);
|
||||
const ext = path.extname(resolvedPath);
|
||||
if (ext) {
|
||||
return [resolvedPath];
|
||||
}
|
||||
return [
|
||||
resolvedPath,
|
||||
`${resolvedPath}.ts`,
|
||||
`${resolvedPath}.js`,
|
||||
`${resolvedPath}.mjs`,
|
||||
`${resolvedPath}.cjs`,
|
||||
];
|
||||
}
|
||||
|
||||
function resolveExistingModulePath(entry: PluginChannelCatalogEntry, specifier: string): string {
|
||||
for (const candidate of resolveModuleCandidates(entry, specifier)) {
|
||||
if (fs.existsSync(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
return path.resolve(entry.rootDir, specifier);
|
||||
}
|
||||
|
||||
function loadPersistedAuthStateModule(modulePath: string, rootDir: string): unknown {
|
||||
const opened = openBoundaryFileSync({
|
||||
absolutePath: modulePath,
|
||||
rootPath: rootDir,
|
||||
boundaryLabel: "plugin root",
|
||||
rejectHardlinks: false,
|
||||
skipLexicalRootCheck: true,
|
||||
});
|
||||
if (!opened.ok) {
|
||||
throw new Error("plugin persisted-auth module escapes plugin root or fails alias checks");
|
||||
}
|
||||
const safePath = opened.path;
|
||||
fs.closeSync(opened.fd);
|
||||
if (
|
||||
process.platform === "win32" &&
|
||||
[".js", ".mjs", ".cjs"].includes(path.extname(safePath).toLowerCase())
|
||||
) {
|
||||
try {
|
||||
return nodeRequire(safePath);
|
||||
} catch {
|
||||
// Fall back to Jiti when native require cannot load the target.
|
||||
}
|
||||
}
|
||||
return loadModule(safePath)(safePath);
|
||||
}
|
||||
|
||||
function resolvePersistedAuthStateChecker(
|
||||
entry: PluginChannelCatalogEntry,
|
||||
): PersistedAuthStateChecker | null {
|
||||
const cached = persistedAuthStateCheckerCache.get(entry.pluginId);
|
||||
if (cached !== undefined) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
const metadata = resolvePersistedAuthStateMetadata(entry);
|
||||
if (!metadata) {
|
||||
persistedAuthStateCheckerCache.set(entry.pluginId, null);
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const moduleExport = loadPersistedAuthStateModule(
|
||||
resolveExistingModulePath(entry, metadata.specifier!),
|
||||
entry.rootDir,
|
||||
) as Record<string, unknown>;
|
||||
const checker = moduleExport[metadata.exportName!] as PersistedAuthStateChecker | undefined;
|
||||
if (typeof checker !== "function") {
|
||||
throw new Error(`missing persisted auth export ${metadata.exportName}`);
|
||||
}
|
||||
persistedAuthStateCheckerCache.set(entry.pluginId, checker);
|
||||
return checker;
|
||||
} catch (error) {
|
||||
const detail = error instanceof Error ? error.message : String(error);
|
||||
log.warn(`[channels] failed to load persisted auth checker for ${entry.pluginId}: ${detail}`);
|
||||
persistedAuthStateCheckerCache.set(entry.pluginId, null);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function listBundledChannelIdsWithPersistedAuthState(): string[] {
|
||||
return persistedAuthStateCatalog.map((entry) => entry.pluginId);
|
||||
}
|
||||
|
||||
export function hasBundledChannelPersistedAuthState(params: {
|
||||
channelId: string;
|
||||
cfg: OpenClawConfig;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}): boolean {
|
||||
const entry = persistedAuthStateEntriesById.get(params.channelId);
|
||||
if (!entry) {
|
||||
return false;
|
||||
}
|
||||
const checker = resolvePersistedAuthStateChecker(entry);
|
||||
return checker ? Boolean(checker({ cfg: params.cfg, env: params.env })) : false;
|
||||
}
|
||||
|
|
@ -2,7 +2,8 @@ import fs from "node:fs";
|
|||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { resolveDefaultAgentId } from "../agents/agent-scope.js";
|
||||
import { listBootstrapChannelPlugins } from "../channels/plugins/bootstrap-registry.js";
|
||||
import { listBundledChannelPluginIds } from "../channels/plugins/bundled-ids.js";
|
||||
import { hasBundledChannelPersistedAuthState } from "../channels/plugins/persisted-auth-state.js";
|
||||
import { formatCliCommand } from "../cli/command-format.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { resolveOAuthDir, resolveStateDir } from "../config/paths.js";
|
||||
|
|
@ -463,8 +464,8 @@ function shouldRequireOAuthDir(cfg: OpenClawConfig, env: NodeJS.ProcessEnv): boo
|
|||
if (!isRecord(channels)) {
|
||||
return false;
|
||||
}
|
||||
for (const plugin of listBootstrapChannelPlugins()) {
|
||||
if (plugin.config.hasPersistedAuthState?.({ cfg, env })) {
|
||||
for (const channelId of listBundledChannelPluginIds()) {
|
||||
if (hasBundledChannelPersistedAuthState({ channelId, cfg, env })) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { listBootstrapChannelPlugins } from "../../../channels/plugins/bootstrap-registry.js";
|
||||
import { iterateBootstrapChannelPlugins } from "../../../channels/plugins/bootstrap-registry.js";
|
||||
import type { OpenClawConfig } from "../../../config/types.js";
|
||||
|
||||
export function applyChannelDoctorCompatibilityMigrations(cfg: Record<string, unknown>): {
|
||||
|
|
@ -7,7 +7,7 @@ export function applyChannelDoctorCompatibilityMigrations(cfg: Record<string, un
|
|||
} {
|
||||
let nextCfg = cfg as OpenClawConfig & Record<string, unknown>;
|
||||
const changes: string[] = [];
|
||||
for (const plugin of listBootstrapChannelPlugins()) {
|
||||
for (const plugin of iterateBootstrapChannelPlugins()) {
|
||||
const mutation = plugin.doctor?.normalizeCompatibilityConfig?.({ cfg: nextCfg });
|
||||
if (!mutation || mutation.changes.length === 0) {
|
||||
continue;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { hasMeaningfulChannelConfig } from "../channels/config-presence.js";
|
||||
import { getBootstrapChannelPlugin } from "../channels/plugins/bootstrap-registry.js";
|
||||
import { hasBundledChannelPersistedAuthState } from "../channels/plugins/persisted-auth-state.js";
|
||||
import { isRecord } from "../utils.js";
|
||||
import type { OpenClawConfig } from "./config.js";
|
||||
|
||||
|
|
@ -27,7 +28,7 @@ export function isChannelConfigured(
|
|||
if (pluginConfigured) {
|
||||
return true;
|
||||
}
|
||||
const pluginPersistedAuthState = plugin?.config?.hasPersistedAuthState?.({ cfg, env });
|
||||
const pluginPersistedAuthState = hasBundledChannelPersistedAuthState({ channelId, cfg, env });
|
||||
if (pluginPersistedAuthState) {
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { listBootstrapChannelPlugins } from "../../channels/plugins/bootstrap-registry.js";
|
||||
import { getBootstrapChannelPlugin } from "../../channels/plugins/bootstrap-registry.js";
|
||||
import type { ChannelMessageActionName } from "../../channels/plugins/types.js";
|
||||
|
||||
export type MessageActionTargetMode = "to" | "channelId" | "none";
|
||||
|
|
@ -91,14 +91,10 @@ function listActionTargetAliasSpecs(
|
|||
if (!normalizedChannel) {
|
||||
return specs;
|
||||
}
|
||||
for (const plugin of listBootstrapChannelPlugins()) {
|
||||
if (plugin.id !== normalizedChannel) {
|
||||
continue;
|
||||
}
|
||||
const channelSpec = plugin.actions?.messageActionTargetAliases?.[action];
|
||||
if (channelSpec) {
|
||||
specs.push(channelSpec);
|
||||
}
|
||||
const plugin = getBootstrapChannelPlugin(normalizedChannel);
|
||||
const channelSpec = plugin?.actions?.messageActionTargetAliases?.[action];
|
||||
if (channelSpec) {
|
||||
specs.push(channelSpec);
|
||||
}
|
||||
return specs;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import fs from "node:fs";
|
|||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { resolveDefaultAgentId } from "../agents/agent-scope.js";
|
||||
import { listBootstrapChannelPlugins } from "../channels/plugins/bootstrap-registry.js";
|
||||
import { iterateBootstrapChannelPlugins } from "../channels/plugins/bootstrap-registry.js";
|
||||
import { listBundledChannelPlugins } from "../channels/plugins/bundled.js";
|
||||
import type { ChannelLegacyStateMigrationPlan } from "../channels/plugins/types.core.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
|
|
@ -79,11 +79,14 @@ type LegacySessionSurface = {
|
|||
};
|
||||
|
||||
function getLegacySessionSurfaces(): LegacySessionSurface[] {
|
||||
return listBootstrapChannelPlugins()
|
||||
.map((plugin) => plugin.messaging)
|
||||
.filter(
|
||||
(surface): surface is LegacySessionSurface => Boolean(surface) && typeof surface === "object",
|
||||
);
|
||||
const surfaces: LegacySessionSurface[] = [];
|
||||
for (const plugin of iterateBootstrapChannelPlugins()) {
|
||||
const surface = plugin.messaging;
|
||||
if (surface && typeof surface === "object") {
|
||||
surfaces.push(surface);
|
||||
}
|
||||
}
|
||||
return surfaces;
|
||||
}
|
||||
|
||||
function isSurfaceGroupKey(key: string): boolean {
|
||||
|
|
|
|||
|
|
@ -438,6 +438,10 @@ export type PluginPackageChannel = {
|
|||
quickstartAllowFrom?: boolean;
|
||||
forceAccountBinding?: boolean;
|
||||
preferSessionLookupForAnnounceTarget?: boolean;
|
||||
persistedAuthState?: {
|
||||
specifier?: string;
|
||||
exportName?: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type PluginPackageInstall = {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,81 @@
|
|||
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const loadConfigMock = vi.fn();
|
||||
const applyPluginAutoEnableMock = vi.fn();
|
||||
const loadOpenClawPluginsMock = vi.fn();
|
||||
|
||||
let loadPluginMetadataRegistrySnapshot: typeof import("./metadata-registry-loader.js").loadPluginMetadataRegistrySnapshot;
|
||||
|
||||
vi.mock("../../config/config.js", () => ({
|
||||
loadConfig: () => loadConfigMock(),
|
||||
}));
|
||||
|
||||
vi.mock("../../config/plugin-auto-enable.js", () => ({
|
||||
applyPluginAutoEnable: (...args: unknown[]) => applyPluginAutoEnableMock(...args),
|
||||
}));
|
||||
|
||||
vi.mock("../loader.js", () => ({
|
||||
loadOpenClawPlugins: (...args: unknown[]) => loadOpenClawPluginsMock(...args),
|
||||
}));
|
||||
|
||||
vi.mock("../../agents/agent-scope.js", () => ({
|
||||
resolveAgentWorkspaceDir: () => "/resolved-workspace",
|
||||
resolveDefaultAgentId: () => "default",
|
||||
}));
|
||||
|
||||
describe("loadPluginMetadataRegistrySnapshot", () => {
|
||||
beforeAll(async () => {
|
||||
({ loadPluginMetadataRegistrySnapshot } = await import("./metadata-registry-loader.js"));
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
loadConfigMock.mockReset();
|
||||
applyPluginAutoEnableMock.mockReset();
|
||||
loadOpenClawPluginsMock.mockReset();
|
||||
loadConfigMock.mockReturnValue({ plugins: {} });
|
||||
applyPluginAutoEnableMock.mockImplementation((params: { config: unknown }) => ({
|
||||
config: params.config,
|
||||
changes: [],
|
||||
autoEnabledReasons: {},
|
||||
}));
|
||||
loadOpenClawPluginsMock.mockReturnValue({ plugins: [], diagnostics: [] });
|
||||
});
|
||||
|
||||
it("defaults to a non-activating validate snapshot", () => {
|
||||
loadPluginMetadataRegistrySnapshot({
|
||||
config: { plugins: {} },
|
||||
activationSourceConfig: { plugins: { allow: ["demo"] } },
|
||||
env: { HOME: "/tmp/openclaw-home" } as NodeJS.ProcessEnv,
|
||||
workspaceDir: "/workspace",
|
||||
onlyPluginIds: ["demo"],
|
||||
});
|
||||
|
||||
expect(loadOpenClawPluginsMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
config: { plugins: {} },
|
||||
activationSourceConfig: { plugins: { allow: ["demo"] } },
|
||||
workspaceDir: "/workspace",
|
||||
env: { HOME: "/tmp/openclaw-home" },
|
||||
onlyPluginIds: ["demo"],
|
||||
cache: false,
|
||||
activate: false,
|
||||
mode: "validate",
|
||||
loadModules: undefined,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("forwards explicit manifest-only requests", () => {
|
||||
loadPluginMetadataRegistrySnapshot({
|
||||
config: { plugins: {} },
|
||||
loadModules: false,
|
||||
});
|
||||
|
||||
expect(loadOpenClawPluginsMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
loadModules: false,
|
||||
mode: "validate",
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -15,6 +15,7 @@ export function loadPluginMetadataRegistrySnapshot(options?: {
|
|||
env?: NodeJS.ProcessEnv;
|
||||
workspaceDir?: string;
|
||||
onlyPluginIds?: string[];
|
||||
loadModules?: boolean;
|
||||
}): PluginRegistry {
|
||||
const env = options?.env ?? process.env;
|
||||
const baseConfig = options?.config ?? loadConfig();
|
||||
|
|
@ -35,11 +36,13 @@ export function loadPluginMetadataRegistrySnapshot(options?: {
|
|||
activationSourceConfig: options?.activationSourceConfig ?? baseConfig,
|
||||
autoEnabledReasons: autoEnabled.autoEnabledReasons,
|
||||
workspaceDir,
|
||||
env,
|
||||
logger,
|
||||
throwOnLoadError: true,
|
||||
cache: false,
|
||||
activate: false,
|
||||
mode: "validate",
|
||||
loadModules: options?.loadModules,
|
||||
...(options?.onlyPluginIds?.length ? { onlyPluginIds: options.onlyPluginIds } : {}),
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import {
|
|||
|
||||
const loadConfigMock = vi.fn();
|
||||
const loadOpenClawPluginsMock = vi.fn();
|
||||
const loadPluginMetadataRegistrySnapshotMock = vi.fn();
|
||||
const applyPluginAutoEnableMock = vi.fn();
|
||||
const resolveBundledProviderCompatPluginIdsMock = vi.fn();
|
||||
const withBundledPluginAllowlistCompatMock = vi.fn();
|
||||
|
|
@ -38,6 +39,11 @@ vi.mock("./loader.js", () => ({
|
|||
loadOpenClawPlugins: (...args: unknown[]) => loadOpenClawPluginsMock(...args),
|
||||
}));
|
||||
|
||||
vi.mock("./runtime/metadata-registry-loader.js", () => ({
|
||||
loadPluginMetadataRegistrySnapshot: (...args: unknown[]) =>
|
||||
loadPluginMetadataRegistrySnapshotMock(...args),
|
||||
}));
|
||||
|
||||
vi.mock("./providers.js", () => ({
|
||||
resolveBundledProviderCompatPluginIds: (...args: unknown[]) =>
|
||||
resolveBundledProviderCompatPluginIdsMock(...args),
|
||||
|
|
@ -69,12 +75,12 @@ vi.mock("../agents/workspace.js", () => ({
|
|||
}));
|
||||
|
||||
function setPluginLoadResult(overrides: Partial<ReturnType<typeof createPluginLoadResult>>) {
|
||||
loadOpenClawPluginsMock.mockReturnValue(
|
||||
createPluginLoadResult({
|
||||
plugins: [],
|
||||
...overrides,
|
||||
}),
|
||||
);
|
||||
const result = createPluginLoadResult({
|
||||
plugins: [],
|
||||
...overrides,
|
||||
});
|
||||
loadOpenClawPluginsMock.mockReturnValue(result);
|
||||
loadPluginMetadataRegistrySnapshotMock.mockReturnValue(result);
|
||||
}
|
||||
|
||||
function setSinglePluginLoadResult(
|
||||
|
|
@ -122,20 +128,31 @@ function expectPluginLoaderCall(params: {
|
|||
);
|
||||
}
|
||||
|
||||
function expectAutoEnabledStatusLoad(params: {
|
||||
rawConfig: unknown;
|
||||
autoEnabledConfig: unknown;
|
||||
autoEnabledReasons?: Record<string, string[]>;
|
||||
function expectMetadataSnapshotLoaderCall(params: {
|
||||
config?: unknown;
|
||||
activationSourceConfig?: unknown;
|
||||
workspaceDir?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
loadModules?: boolean;
|
||||
}) {
|
||||
expect(loadPluginMetadataRegistrySnapshotMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
...(params.config !== undefined ? { config: params.config } : {}),
|
||||
...(params.activationSourceConfig !== undefined
|
||||
? { activationSourceConfig: params.activationSourceConfig }
|
||||
: {}),
|
||||
...(params.workspaceDir ? { workspaceDir: params.workspaceDir } : {}),
|
||||
...(params.env ? { env: params.env } : {}),
|
||||
...(params.loadModules !== undefined ? { loadModules: params.loadModules } : {}),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
function expectAutoEnabledStatusLoad(params: { rawConfig: unknown }) {
|
||||
expect(applyPluginAutoEnableMock).toHaveBeenCalledWith({
|
||||
config: params.rawConfig,
|
||||
env: process.env,
|
||||
});
|
||||
expectPluginLoaderCall({
|
||||
config: params.autoEnabledConfig,
|
||||
activationSourceConfig: params.rawConfig,
|
||||
autoEnabledReasons: params.autoEnabledReasons ?? {},
|
||||
});
|
||||
}
|
||||
|
||||
function createCompatChainFixture() {
|
||||
|
|
@ -159,6 +176,7 @@ function expectBundledCompatChainApplied(params: {
|
|||
pluginIds: string[];
|
||||
compatConfig: unknown;
|
||||
enabledConfig: unknown;
|
||||
loadModules: boolean;
|
||||
}) {
|
||||
expect(withBundledPluginAllowlistCompatMock).toHaveBeenCalledWith({
|
||||
config: params.config,
|
||||
|
|
@ -168,7 +186,11 @@ function expectBundledCompatChainApplied(params: {
|
|||
config: params.compatConfig,
|
||||
pluginIds: params.pluginIds,
|
||||
});
|
||||
expectPluginLoaderCall({ config: params.enabledConfig });
|
||||
if (params.loadModules) {
|
||||
expectPluginLoaderCall({ config: params.enabledConfig, loadModules: true });
|
||||
return;
|
||||
}
|
||||
expectMetadataSnapshotLoaderCall({ config: params.enabledConfig, loadModules: false });
|
||||
}
|
||||
|
||||
function createAutoEnabledStatusConfig(
|
||||
|
|
@ -258,6 +280,7 @@ describe("plugin status reports", () => {
|
|||
beforeEach(() => {
|
||||
loadConfigMock.mockReset();
|
||||
loadOpenClawPluginsMock.mockReset();
|
||||
loadPluginMetadataRegistrySnapshotMock.mockReset();
|
||||
applyPluginAutoEnableMock.mockReset();
|
||||
resolveBundledProviderCompatPluginIdsMock.mockReset();
|
||||
withBundledPluginAllowlistCompatMock.mockReset();
|
||||
|
|
@ -291,7 +314,7 @@ describe("plugin status reports", () => {
|
|||
env,
|
||||
});
|
||||
|
||||
expectPluginLoaderCall({
|
||||
expectMetadataSnapshotLoaderCall({
|
||||
config: {},
|
||||
workspaceDir: "/workspace",
|
||||
env,
|
||||
|
|
@ -299,16 +322,15 @@ describe("plugin status reports", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it("uses a non-activating snapshot load for snapshot reports", () => {
|
||||
it("uses a metadata snapshot load for snapshot reports", () => {
|
||||
buildPluginSnapshotReport({ config: {}, workspaceDir: "/workspace" });
|
||||
|
||||
expect(loadOpenClawPluginsMock).toHaveBeenCalledWith(
|
||||
expect(loadPluginMetadataRegistrySnapshotMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
activate: false,
|
||||
cache: false,
|
||||
loadModules: false,
|
||||
}),
|
||||
);
|
||||
expect(loadOpenClawPluginsMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("loads plugin status from the auto-enabled config snapshot", () => {
|
||||
|
|
@ -330,12 +352,12 @@ describe("plugin status reports", () => {
|
|||
|
||||
expectAutoEnabledStatusLoad({
|
||||
rawConfig,
|
||||
autoEnabledConfig,
|
||||
autoEnabledReasons: {
|
||||
demo: ["demo configured"],
|
||||
},
|
||||
});
|
||||
expectPluginLoaderCall({ loadModules: false });
|
||||
expectMetadataSnapshotLoaderCall({
|
||||
config: autoEnabledConfig,
|
||||
activationSourceConfig: rawConfig,
|
||||
loadModules: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("uses the auto-enabled config snapshot for inspect policy summaries", () => {
|
||||
|
|
@ -415,12 +437,15 @@ describe("plugin status reports", () => {
|
|||
|
||||
expectAutoEnabledStatusLoad({
|
||||
rawConfig,
|
||||
autoEnabledConfig,
|
||||
});
|
||||
expectPluginLoaderCall({
|
||||
config: autoEnabledConfig,
|
||||
activationSourceConfig: rawConfig,
|
||||
autoEnabledReasons: {
|
||||
demo: ["demo configured"],
|
||||
},
|
||||
loadModules: true,
|
||||
});
|
||||
expectPluginLoaderCall({ loadModules: true });
|
||||
});
|
||||
|
||||
it("applies the full bundled provider compat chain before loading plugins", () => {
|
||||
|
|
@ -437,6 +462,7 @@ describe("plugin status reports", () => {
|
|||
pluginIds,
|
||||
compatConfig,
|
||||
enabledConfig,
|
||||
loadModules: false,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -474,10 +500,14 @@ describe("plugin status reports", () => {
|
|||
|
||||
expectAutoEnabledStatusLoad({
|
||||
rawConfig,
|
||||
autoEnabledConfig,
|
||||
});
|
||||
expectPluginLoaderCall({
|
||||
config: autoEnabledConfig,
|
||||
activationSourceConfig: rawConfig,
|
||||
autoEnabledReasons: {
|
||||
demo: ["demo configured"],
|
||||
},
|
||||
loadModules: true,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import { createPluginLoaderLogger } from "./logger.js";
|
|||
import { resolveBundledProviderCompatPluginIds } from "./providers.js";
|
||||
import type { PluginRegistry } from "./registry.js";
|
||||
import { listImportedRuntimePluginIds } from "./runtime.js";
|
||||
import { loadPluginMetadataRegistrySnapshot } from "./runtime/metadata-registry-loader.js";
|
||||
import type { PluginDiagnostic, PluginHookName } from "./types.js";
|
||||
|
||||
export type PluginStatusReport = PluginRegistry & {
|
||||
|
|
@ -189,17 +190,25 @@ function buildPluginReport(
|
|||
pluginIds: bundledProviderIds,
|
||||
});
|
||||
|
||||
const registry = loadOpenClawPlugins({
|
||||
config: runtimeCompatConfig,
|
||||
activationSourceConfig: rawConfig,
|
||||
autoEnabledReasons: autoEnabled.autoEnabledReasons,
|
||||
workspaceDir,
|
||||
env: params?.env,
|
||||
logger: createPluginLoaderLogger(log),
|
||||
activate: false,
|
||||
cache: false,
|
||||
loadModules,
|
||||
});
|
||||
const registry = loadModules
|
||||
? loadOpenClawPlugins({
|
||||
config: runtimeCompatConfig,
|
||||
activationSourceConfig: rawConfig,
|
||||
autoEnabledReasons: autoEnabled.autoEnabledReasons,
|
||||
workspaceDir,
|
||||
env: params?.env,
|
||||
logger: createPluginLoaderLogger(log),
|
||||
activate: false,
|
||||
cache: false,
|
||||
loadModules,
|
||||
})
|
||||
: loadPluginMetadataRegistrySnapshot({
|
||||
config: runtimeCompatConfig,
|
||||
activationSourceConfig: rawConfig,
|
||||
workspaceDir,
|
||||
env: params?.env,
|
||||
loadModules: false,
|
||||
});
|
||||
const importedPluginIds = new Set([
|
||||
...(loadModules
|
||||
? registry.plugins
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { listBootstrapChannelPlugins } from "../channels/plugins/bootstrap-registry.js";
|
||||
import { iterateBootstrapChannelPlugins } from "../channels/plugins/bootstrap-registry.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { type ResolverContext, type SecretDefaults } from "./runtime-shared.js";
|
||||
|
||||
|
|
@ -7,7 +7,7 @@ export function collectChannelConfigAssignments(params: {
|
|||
defaults: SecretDefaults | undefined;
|
||||
context: ResolverContext;
|
||||
}): void {
|
||||
for (const plugin of listBootstrapChannelPlugins()) {
|
||||
for (const plugin of iterateBootstrapChannelPlugins()) {
|
||||
plugin.secrets?.collectRuntimeConfigAssignments?.(params);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,15 @@
|
|||
import { listBootstrapChannelPlugins } from "../channels/plugins/bootstrap-registry.js";
|
||||
import { iterateBootstrapChannelPlugins } from "../channels/plugins/bootstrap-registry.js";
|
||||
import type { SecretTargetRegistryEntry } from "./target-registry-types.js";
|
||||
|
||||
const SECRET_INPUT_SHAPE = "secret_input"; // pragma: allowlist secret
|
||||
const SIBLING_REF_SHAPE = "sibling_ref"; // pragma: allowlist secret
|
||||
|
||||
function listChannelSecretTargetRegistryEntries(): SecretTargetRegistryEntry[] {
|
||||
return listBootstrapChannelPlugins().flatMap(
|
||||
(plugin) => plugin.secrets?.secretTargetRegistryEntries ?? [],
|
||||
);
|
||||
const entries: SecretTargetRegistryEntry[] = [];
|
||||
for (const plugin of iterateBootstrapChannelPlugins()) {
|
||||
entries.push(...(plugin.secrets?.secretTargetRegistryEntries ?? []));
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
const CORE_SECRET_TARGET_REGISTRY: SecretTargetRegistryEntry[] = [
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { listBootstrapChannelPlugins } from "../channels/plugins/bootstrap-registry.js";
|
||||
import { iterateBootstrapChannelPlugins } from "../channels/plugins/bootstrap-registry.js";
|
||||
import { isRecord } from "../utils.js";
|
||||
|
||||
const CORE_UNSUPPORTED_SECRETREF_SURFACE_PATTERNS = [
|
||||
|
|
@ -10,9 +10,11 @@ const CORE_UNSUPPORTED_SECRETREF_SURFACE_PATTERNS = [
|
|||
] as const;
|
||||
|
||||
function collectChannelUnsupportedSecretRefSurfacePatterns(): string[] {
|
||||
return listBootstrapChannelPlugins().flatMap(
|
||||
(plugin) => plugin.secrets?.unsupportedSecretRefSurfacePatterns ?? [],
|
||||
);
|
||||
const patterns: string[] = [];
|
||||
for (const plugin of iterateBootstrapChannelPlugins()) {
|
||||
patterns.push(...(plugin.secrets?.unsupportedSecretRefSurfacePatterns ?? []));
|
||||
}
|
||||
return patterns;
|
||||
}
|
||||
|
||||
let cachedUnsupportedSecretRefSurfacePatterns: string[] | null = null;
|
||||
|
|
@ -74,7 +76,7 @@ export function collectUnsupportedSecretRefConfigCandidates(
|
|||
}
|
||||
|
||||
if (isRecord(raw.channels)) {
|
||||
for (const plugin of listBootstrapChannelPlugins()) {
|
||||
for (const plugin of iterateBootstrapChannelPlugins()) {
|
||||
const channelCandidates = plugin.secrets?.collectUnsupportedSecretRefConfigCandidates?.(raw);
|
||||
if (!channelCandidates?.length) {
|
||||
continue;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { listBootstrapChannelPlugins } from "../channels/plugins/bootstrap-registry.js";
|
||||
import { iterateBootstrapChannelPlugins } from "../channels/plugins/bootstrap-registry.js";
|
||||
import { parseAgentSessionKey } from "./session-key-utils.js";
|
||||
|
||||
export type SessionKeyChatType = "direct" | "group" | "channel" | "unknown";
|
||||
|
|
@ -44,7 +44,7 @@ export function deriveSessionChatType(sessionKey: string | undefined | null): Se
|
|||
if (builtInLegacy) {
|
||||
return builtInLegacy;
|
||||
}
|
||||
for (const plugin of listBootstrapChannelPlugins()) {
|
||||
for (const plugin of iterateBootstrapChannelPlugins()) {
|
||||
const derived = plugin.messaging?.deriveLegacySessionChatType?.(scoped);
|
||||
if (derived) {
|
||||
return derived;
|
||||
|
|
|
|||
Loading…
Reference in New Issue