diff --git a/extensions/discord/src/approval-native.ts b/extensions/discord/src/approval-native.ts index f043b481815..ca8369c8cb7 100644 --- a/extensions/discord/src/approval-native.ts +++ b/extensions/discord/src/approval-native.ts @@ -1,3 +1,6 @@ +import type { DiscordExecApprovalConfig, OpenClawConfig } from "openclaw/plugin-sdk/config-runtime"; +import type { ExecApprovalRequest, PluginApprovalRequest } from "openclaw/plugin-sdk/infra-runtime"; +import { listDiscordAccountIds, resolveDiscordAccount } from "./accounts.js"; import { createChannelApproverDmTargetResolver, createChannelNativeOriginTargetResolver, @@ -6,10 +9,7 @@ import { doesApprovalRequestMatchChannelAccount, isChannelExecApprovalClientEnabledFromConfig, matchesApprovalRequestFilters, -} from "openclaw/plugin-sdk/approval-runtime"; -import type { DiscordExecApprovalConfig, OpenClawConfig } from "openclaw/plugin-sdk/config-runtime"; -import type { ExecApprovalRequest, PluginApprovalRequest } from "openclaw/plugin-sdk/infra-runtime"; -import { listDiscordAccountIds, resolveDiscordAccount } from "./accounts.js"; +} from "./approval-runtime.js"; import { getDiscordExecApprovalApprovers, isDiscordExecApprovalApprover, diff --git a/extensions/discord/src/approval-runtime.ts b/extensions/discord/src/approval-runtime.ts new file mode 100644 index 00000000000..a65b8c06ab7 --- /dev/null +++ b/extensions/discord/src/approval-runtime.ts @@ -0,0 +1,15 @@ +export { + isChannelExecApprovalClientEnabledFromConfig, + matchesApprovalRequestFilters, + getExecApprovalReplyMetadata, +} from "openclaw/plugin-sdk/approval-client-runtime"; +export { resolveApprovalApprovers } from "openclaw/plugin-sdk/approval-auth-runtime"; +export { + createApproverRestrictedNativeApprovalCapability, + splitChannelApprovalCapability, +} from "openclaw/plugin-sdk/approval-delivery-runtime"; +export { + createChannelApproverDmTargetResolver, + createChannelNativeOriginTargetResolver, + doesApprovalRequestMatchChannelAccount, +} from "openclaw/plugin-sdk/approval-native-runtime"; diff --git a/extensions/discord/src/channel.setup.ts b/extensions/discord/src/channel.setup.ts index 36e493c83db..71d95e33340 100644 --- a/extensions/discord/src/channel.setup.ts +++ b/extensions/discord/src/channel.setup.ts @@ -1,10 +1,12 @@ import { type ResolvedDiscordAccount } from "./accounts.js"; import { type ChannelPlugin } from "./channel-api.js"; -import { discordSetupAdapter } from "./setup-core.js"; +import { discordSetupWizard } from "./channel.runtime.js"; +import { discordSetupAdapter } from "./setup-adapter.js"; import { createDiscordPluginBase } from "./shared.js"; export const discordSetupPlugin: ChannelPlugin = { ...createDiscordPluginBase({ + setupWizard: discordSetupWizard, setup: discordSetupAdapter, }), }; diff --git a/extensions/discord/src/channel.ts b/extensions/discord/src/channel.ts index 93bf063c6af..349e9384432 100644 --- a/extensions/discord/src/channel.ts +++ b/extensions/discord/src/channel.ts @@ -63,7 +63,7 @@ import { resolveDiscordOutboundSessionRoute } from "./outbound-session-route.js" import type { DiscordProbe } from "./probe.js"; import { getDiscordRuntime } from "./runtime.js"; import { normalizeExplicitDiscordSessionKey } from "./session-key-normalization.js"; -import { discordSetupAdapter } from "./setup-core.js"; +import { discordSetupAdapter } from "./setup-adapter.js"; import { createDiscordPluginBase, discordConfigAdapter } from "./shared.js"; import { collectDiscordStatusIssues } from "./status-issues.js"; import { parseDiscordTarget } from "./target-parsing.js"; diff --git a/extensions/discord/src/doctor-shared.ts b/extensions/discord/src/doctor-shared.ts new file mode 100644 index 00000000000..f9700acf039 --- /dev/null +++ b/extensions/discord/src/doctor-shared.ts @@ -0,0 +1,44 @@ +import type { ChannelDoctorLegacyConfigRule } from "openclaw/plugin-sdk/channel-contract"; +import { resolveDiscordPreviewStreamMode } from "./preview-streaming.js"; + +function asObjectRecord(value: unknown): Record | null { + return value && typeof value === "object" && !Array.isArray(value) + ? (value as Record) + : null; +} + +function hasLegacyDiscordStreamingAliases(value: unknown): boolean { + const entry = asObjectRecord(value); + if (!entry) { + return false; + } + return ( + entry.streamMode !== undefined || + typeof entry.streaming === "boolean" || + (typeof entry.streaming === "string" && + entry.streaming !== resolveDiscordPreviewStreamMode(entry)) + ); +} + +function hasLegacyDiscordAccountStreamingAliases(value: unknown): boolean { + const accounts = asObjectRecord(value); + if (!accounts) { + return false; + } + return Object.values(accounts).some((account) => hasLegacyDiscordStreamingAliases(account)); +} + +export const DISCORD_LEGACY_CONFIG_RULES: ChannelDoctorLegacyConfigRule[] = [ + { + path: ["channels", "discord"], + message: + "channels.discord.streamMode and boolean channels.discord.streaming are legacy; use channels.discord.streaming.", + match: hasLegacyDiscordStreamingAliases, + }, + { + path: ["channels", "discord", "accounts"], + message: + "channels.discord.accounts..streamMode and boolean channels.discord.accounts..streaming are legacy; use channels.discord.accounts..streaming.", + match: hasLegacyDiscordAccountStreamingAliases, + }, +]; diff --git a/extensions/discord/src/doctor.ts b/extensions/discord/src/doctor.ts index 1d48c525f65..42a1a8db868 100644 --- a/extensions/discord/src/doctor.ts +++ b/extensions/discord/src/doctor.ts @@ -1,12 +1,11 @@ import { type ChannelDoctorAdapter, type ChannelDoctorConfigMutation, - type ChannelDoctorLegacyConfigRule, } from "openclaw/plugin-sdk/channel-contract"; import { type OpenClawConfig } from "openclaw/plugin-sdk/config-runtime"; import { collectProviderDangerousNameMatchingScopes } from "openclaw/plugin-sdk/runtime"; +import { DISCORD_LEGACY_CONFIG_RULES } from "./doctor-shared.js"; import { resolveDiscordPreviewStreamMode } from "./preview-streaming.js"; -import { isDiscordMutableAllowEntry } from "./security-audit.js"; type DiscordNumericIdHit = { path: string; entry: number; safe: boolean }; @@ -26,6 +25,27 @@ function sanitizeForLog(value: string): string { return value.replace(/[\u0000-\u001f\u007f]+/g, " ").trim(); } +function isDiscordMutableAllowEntry(raw: string): boolean { + const text = raw.trim(); + if (!text || text === "*") { + return false; + } + + const maybeMentionId = text.replace(/^<@!?/, "").replace(/>$/, ""); + if (/^\d+$/.test(maybeMentionId)) { + return false; + } + + for (const prefix of ["discord:", "user:", "pk:"]) { + if (!text.startsWith(prefix)) { + continue; + } + return text.slice(prefix.length).trim().length === 0; + } + + return true; +} + function normalizeDiscordDmAliases(params: { entry: Record; pathPrefix: string; @@ -514,42 +534,6 @@ function collectDiscordMutableAllowlistWarnings(cfg: OpenClawConfig): string[] { ]; } -function hasLegacyDiscordStreamingAliases(value: unknown): boolean { - const entry = asObjectRecord(value); - if (!entry) { - return false; - } - return ( - entry.streamMode !== undefined || - typeof entry.streaming === "boolean" || - (typeof entry.streaming === "string" && - entry.streaming !== resolveDiscordPreviewStreamMode(entry)) - ); -} - -function hasLegacyDiscordAccountStreamingAliases(value: unknown): boolean { - const accounts = asObjectRecord(value); - if (!accounts) { - return false; - } - return Object.values(accounts).some((account) => hasLegacyDiscordStreamingAliases(account)); -} - -const DISCORD_LEGACY_CONFIG_RULES: ChannelDoctorLegacyConfigRule[] = [ - { - path: ["channels", "discord"], - message: - "channels.discord.streamMode and boolean channels.discord.streaming are legacy; use channels.discord.streaming.", - match: hasLegacyDiscordStreamingAliases, - }, - { - path: ["channels", "discord", "accounts"], - message: - "channels.discord.accounts..streamMode and boolean channels.discord.accounts..streaming are legacy; use channels.discord.accounts..streaming.", - match: hasLegacyDiscordAccountStreamingAliases, - }, -]; - export const discordDoctor: ChannelDoctorAdapter = { dmAllowFromMode: "topOrNested", groupModel: "route", diff --git a/extensions/discord/src/exec-approvals.ts b/extensions/discord/src/exec-approvals.ts index 3b1b0ef3096..c1796d9aa8d 100644 --- a/extensions/discord/src/exec-approvals.ts +++ b/extensions/discord/src/exec-approvals.ts @@ -1,10 +1,12 @@ -import { getExecApprovalReplyMetadata } from "openclaw/plugin-sdk/approval-runtime"; -import { isChannelExecApprovalClientEnabledFromConfig } from "openclaw/plugin-sdk/approval-runtime"; -import { resolveApprovalApprovers } from "openclaw/plugin-sdk/approval-runtime"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime"; import type { DiscordExecApprovalConfig } from "openclaw/plugin-sdk/config-runtime"; import type { ReplyPayload } from "openclaw/plugin-sdk/reply-dispatch-runtime"; import { resolveDiscordAccount } from "./accounts.js"; +import { + getExecApprovalReplyMetadata, + isChannelExecApprovalClientEnabledFromConfig, + resolveApprovalApprovers, +} from "./approval-runtime.js"; import { parseDiscordTarget } from "./target-parsing.js"; function normalizeDiscordApproverId(value: string): string | undefined { diff --git a/extensions/discord/src/monitor/thread-bindings.manager.ts b/extensions/discord/src/monitor/thread-bindings.manager.ts index f483d90d483..e896190e257 100644 --- a/extensions/discord/src/monitor/thread-bindings.manager.ts +++ b/extensions/discord/src/monitor/thread-bindings.manager.ts @@ -1,5 +1,4 @@ import { Routes } from "discord-api-types/v10"; -import { getRuntimeConfigSnapshot, type OpenClawConfig } from "openclaw/plugin-sdk/config-runtime"; import { registerSessionBindingAdapter, resolveThreadBindingConversationIdFromBindingId, @@ -9,6 +8,10 @@ import { type SessionBindingRecord, } from "openclaw/plugin-sdk/conversation-runtime"; import { normalizeAccountId, resolveAgentIdFromSessionKey } from "openclaw/plugin-sdk/routing"; +import { + getRuntimeConfigSnapshot, + type OpenClawConfig, +} from "openclaw/plugin-sdk/runtime-config-snapshot"; import { logVerbose } from "openclaw/plugin-sdk/runtime-env"; import { createDiscordRestClient } from "../client.js"; import { diff --git a/extensions/discord/src/setup-adapter.ts b/extensions/discord/src/setup-adapter.ts new file mode 100644 index 00000000000..be3b2aa8938 --- /dev/null +++ b/extensions/discord/src/setup-adapter.ts @@ -0,0 +1,12 @@ +import { createEnvPatchedAccountSetupAdapter } from "openclaw/plugin-sdk/setup-adapter-runtime"; +import type { ChannelSetupAdapter } from "openclaw/plugin-sdk/setup-runtime"; + +const channel = "discord" as const; + +export const discordSetupAdapter: ChannelSetupAdapter = createEnvPatchedAccountSetupAdapter({ + channelKey: channel, + defaultAccountOnlyEnvError: "DISCORD_BOT_TOKEN can only be used for the default account.", + missingCredentialError: "Discord requires token (or --use-env).", + hasCredentials: (input) => Boolean(input.token), + buildPatch: (input) => (input.token ? { token: input.token } : {}), +}); diff --git a/extensions/discord/src/setup-core.ts b/extensions/discord/src/setup-core.ts index 30a05a94a1d..903c61b09aa 100644 --- a/extensions/discord/src/setup-core.ts +++ b/extensions/discord/src/setup-core.ts @@ -1,12 +1,7 @@ import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/account-id"; import type { DiscordGuildEntry } from "openclaw/plugin-sdk/config-runtime"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime"; -import { createEnvPatchedAccountSetupAdapter } from "openclaw/plugin-sdk/setup-adapter-runtime"; -import type { - ChannelSetupAdapter, - ChannelSetupDmPolicy, - ChannelSetupWizard, -} from "openclaw/plugin-sdk/setup-runtime"; +import type { ChannelSetupDmPolicy, ChannelSetupWizard } from "openclaw/plugin-sdk/setup-runtime"; import { createStandardChannelSetupStatus } from "openclaw/plugin-sdk/setup-runtime"; import { formatDocsLink } from "openclaw/plugin-sdk/setup-tools"; import { @@ -14,6 +9,7 @@ import { listDiscordSetupAccountIds, resolveDiscordSetupAccountConfig, } from "./setup-account-state.js"; +import { discordSetupAdapter } from "./setup-adapter.js"; import { createAccountScopedAllowFromSection, createAccountScopedGroupAccessSection, @@ -75,14 +71,6 @@ export function parseDiscordAllowFromId(value: string): string | null { }); } -export const discordSetupAdapter: ChannelSetupAdapter = createEnvPatchedAccountSetupAdapter({ - channelKey: channel, - defaultAccountOnlyEnvError: "DISCORD_BOT_TOKEN can only be used for the default account.", - missingCredentialError: "Discord requires token (or --use-env).", - hasCredentials: (input) => Boolean(input.token), - buildPatch: (input) => (input.token ? { token: input.token } : {}), -}); - export function createDiscordSetupWizardBase(handlers: { promptAllowFrom: NonNullable; resolveAllowFromEntries: NonNullable< diff --git a/extensions/discord/src/shared.ts b/extensions/discord/src/shared.ts index 8c92f19991f..6e5b875af16 100644 --- a/extensions/discord/src/shared.ts +++ b/extensions/discord/src/shared.ts @@ -2,6 +2,7 @@ import { describeAccountSnapshot } from "openclaw/plugin-sdk/account-helpers"; import { formatAllowFromLowercase } from "openclaw/plugin-sdk/allow-from"; import { adaptScopedAccountAccessor } from "openclaw/plugin-sdk/channel-config-helpers"; import { createScopedChannelConfigAdapter } from "openclaw/plugin-sdk/channel-config-helpers"; +import type { ChannelDoctorAdapter } from "openclaw/plugin-sdk/channel-contract"; import { inspectDiscordAccount } from "./account-inspect.js"; import { listDiscordAccountIds, @@ -11,18 +12,40 @@ import { } from "./accounts.js"; import { getChatChannelMeta, type ChannelPlugin } from "./channel-api.js"; import { DiscordChannelConfigSchema } from "./config-schema.js"; -import { discordDoctor } from "./doctor.js"; -import { createDiscordSetupWizardProxy } from "./setup-core.js"; +import { DISCORD_LEGACY_CONFIG_RULES } from "./doctor-shared.js"; export const DISCORD_CHANNEL = "discord" as const; -async function loadDiscordChannelRuntime() { - return await import("./channel.runtime.js"); +type DiscordDoctorModule = typeof import("./doctor.js"); + +let discordDoctorModulePromise: Promise | undefined; + +async function loadDiscordDoctorModule(): Promise { + discordDoctorModulePromise ??= import("./doctor.js"); + return await discordDoctorModulePromise; } -export const discordSetupWizard = createDiscordSetupWizardProxy( - async () => (await loadDiscordChannelRuntime()).discordSetupWizard, -); +const discordDoctor: ChannelDoctorAdapter = { + dmAllowFromMode: "topOrNested", + groupModel: "route", + groupAllowFromFallbackToAllowFrom: false, + warnOnEmptyGroupSenderAllowlist: false, + legacyConfigRules: DISCORD_LEGACY_CONFIG_RULES, + normalizeCompatibilityConfig: async (params) => + (await loadDiscordDoctorModule()).discordDoctor.normalizeCompatibilityConfig?.(params) ?? { + config: params.cfg, + changes: [], + }, + collectPreviewWarnings: async (params) => + (await loadDiscordDoctorModule()).discordDoctor.collectPreviewWarnings?.(params) ?? [], + collectMutableAllowlistWarnings: async (params) => + (await loadDiscordDoctorModule()).discordDoctor.collectMutableAllowlistWarnings?.(params) ?? [], + repairConfig: async (params) => + (await loadDiscordDoctorModule()).discordDoctor.repairConfig?.(params) ?? { + config: params.cfg, + changes: [], + }, +}; export const discordConfigAdapter = createScopedChannelConfigAdapter({ sectionKey: DISCORD_CHANNEL, @@ -38,6 +61,7 @@ export const discordConfigAdapter = createScopedChannelConfigAdapter["setup"]>; + setupWizard?: ChannelPlugin["setupWizard"]; }): Pick< ChannelPlugin, | "id" @@ -54,7 +78,7 @@ export function createDiscordPluginBase(params: { > { return { id: DISCORD_CHANNEL, - setupWizard: discordSetupWizard, + ...(params.setupWizard ? { setupWizard: params.setupWizard } : {}), meta: { ...getChatChannelMeta(DISCORD_CHANNEL) }, capabilities: { chatTypes: ["direct", "channel", "thread"], diff --git a/extensions/feishu/channel-entry.ts b/extensions/feishu/channel-entry.ts new file mode 100644 index 00000000000..b72338740ab --- /dev/null +++ b/extensions/feishu/channel-entry.ts @@ -0,0 +1,14 @@ +import { defineChannelPluginEntry } from "openclaw/plugin-sdk/channel-core"; +import { feishuPlugin } from "./src/channel.js"; +import { setFeishuRuntime } from "./src/runtime.js"; + +export { feishuPlugin } from "./src/channel.js"; +export { setFeishuRuntime } from "./src/runtime.js"; + +export default defineChannelPluginEntry({ + id: "feishu", + name: "Feishu", + description: "Feishu/Lark channel plugin", + plugin: feishuPlugin, + setRuntime: setFeishuRuntime, +}); diff --git a/extensions/feishu/src/approval-auth.ts b/extensions/feishu/src/approval-auth.ts index c0ad682a2b8..dc595b4c0e0 100644 --- a/extensions/feishu/src/approval-auth.ts +++ b/extensions/feishu/src/approval-auth.ts @@ -1,7 +1,7 @@ import { createResolvedApproverActionAuthAdapter, resolveApprovalApprovers, -} from "openclaw/plugin-sdk/approval-runtime"; +} from "openclaw/plugin-sdk/approval-auth-runtime"; import { resolveFeishuAccount } from "./accounts.js"; import { normalizeFeishuTarget } from "./targets.js"; diff --git a/extensions/googlechat/src/approval-auth.ts b/extensions/googlechat/src/approval-auth.ts index fcb47c32c8b..84154718cd7 100644 --- a/extensions/googlechat/src/approval-auth.ts +++ b/extensions/googlechat/src/approval-auth.ts @@ -1,7 +1,7 @@ import { createResolvedApproverActionAuthAdapter, resolveApprovalApprovers, -} from "openclaw/plugin-sdk/approval-runtime"; +} from "openclaw/plugin-sdk/approval-auth-runtime"; import { resolveGoogleChatAccount } from "./accounts.js"; import { isGoogleChatUserTarget, normalizeGoogleChatTarget } from "./targets.js"; diff --git a/extensions/matrix/src/approval-auth.ts b/extensions/matrix/src/approval-auth.ts index cde3d1b324b..a4029ef689b 100644 --- a/extensions/matrix/src/approval-auth.ts +++ b/extensions/matrix/src/approval-auth.ts @@ -1,7 +1,7 @@ import { createResolvedApproverActionAuthAdapter, resolveApprovalApprovers, -} from "openclaw/plugin-sdk/approval-runtime"; +} from "openclaw/plugin-sdk/approval-auth-runtime"; import { normalizeMatrixApproverId } from "./exec-approvals.js"; import { resolveMatrixAccount } from "./matrix/accounts.js"; import type { CoreConfig } from "./types.js"; diff --git a/extensions/matrix/src/approval-native.ts b/extensions/matrix/src/approval-native.ts index f142d63bf26..5c4e46b3d23 100644 --- a/extensions/matrix/src/approval-native.ts +++ b/extensions/matrix/src/approval-native.ts @@ -1,10 +1,12 @@ import { createChannelApprovalCapability, - createChannelApproverDmTargetResolver, - createChannelNativeOriginTargetResolver, createApproverRestrictedNativeApprovalCapability, splitChannelApprovalCapability, -} from "openclaw/plugin-sdk/approval-runtime"; +} from "openclaw/plugin-sdk/approval-delivery-runtime"; +import { + createChannelApproverDmTargetResolver, + createChannelNativeOriginTargetResolver, +} from "openclaw/plugin-sdk/approval-native-runtime"; import type { ExecApprovalRequest, PluginApprovalRequest } from "openclaw/plugin-sdk/infra-runtime"; import { getMatrixApprovalAuthApprovers, matrixApprovalAuth } from "./approval-auth.js"; import { diff --git a/extensions/matrix/src/exec-approvals-handler.ts b/extensions/matrix/src/exec-approvals-handler.ts index e2a514c5f72..d8f29a76708 100644 --- a/extensions/matrix/src/exec-approvals-handler.ts +++ b/extensions/matrix/src/exec-approvals-handler.ts @@ -2,7 +2,7 @@ import { buildExecApprovalPendingReplyPayload, getExecApprovalApproverDmNoticeText, resolveExecApprovalCommandDisplay, -} from "openclaw/plugin-sdk/approval-runtime"; +} from "openclaw/plugin-sdk/approval-reply-runtime"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime"; import { createChannelNativeApprovalRuntime, diff --git a/extensions/matrix/src/exec-approvals.ts b/extensions/matrix/src/exec-approvals.ts index 2fea05824d9..594da1421c0 100644 --- a/extensions/matrix/src/exec-approvals.ts +++ b/extensions/matrix/src/exec-approvals.ts @@ -1,12 +1,12 @@ +import { resolveApprovalApprovers } from "openclaw/plugin-sdk/approval-auth-runtime"; import { createChannelExecApprovalProfile, getExecApprovalReplyMetadata, isChannelExecApprovalClientEnabledFromConfig, isChannelExecApprovalTargetRecipient, matchesApprovalRequestFilters, - resolveApprovalRequestChannelAccountId, - resolveApprovalApprovers, -} from "openclaw/plugin-sdk/approval-runtime"; +} from "openclaw/plugin-sdk/approval-client-runtime"; +import { resolveApprovalRequestChannelAccountId } from "openclaw/plugin-sdk/approval-native-runtime"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime"; import type { ExecApprovalRequest, PluginApprovalRequest } from "openclaw/plugin-sdk/infra-runtime"; import type { ReplyPayload } from "openclaw/plugin-sdk/reply-runtime"; diff --git a/extensions/mattermost/src/approval-auth.ts b/extensions/mattermost/src/approval-auth.ts index 411c254e146..6016dbf1d85 100644 --- a/extensions/mattermost/src/approval-auth.ts +++ b/extensions/mattermost/src/approval-auth.ts @@ -1,7 +1,7 @@ import { createResolvedApproverActionAuthAdapter, resolveApprovalApprovers, -} from "openclaw/plugin-sdk/approval-runtime"; +} from "openclaw/plugin-sdk/approval-auth-runtime"; import { resolveMattermostAccount } from "./mattermost/accounts.js"; const MATTERMOST_USER_ID_RE = /^[a-z0-9]{26}$/; diff --git a/extensions/msteams/src/approval-auth.ts b/extensions/msteams/src/approval-auth.ts index d5c21ca2b0c..1eca5a93369 100644 --- a/extensions/msteams/src/approval-auth.ts +++ b/extensions/msteams/src/approval-auth.ts @@ -1,7 +1,7 @@ import { createResolvedApproverActionAuthAdapter, resolveApprovalApprovers, -} from "openclaw/plugin-sdk/approval-runtime"; +} from "openclaw/plugin-sdk/approval-auth-runtime"; import type { OpenClawConfig } from "../runtime-api.js"; import { normalizeMSTeamsMessagingTarget } from "./resolve-allowlist.js"; diff --git a/extensions/nextcloud-talk/src/approval-auth.ts b/extensions/nextcloud-talk/src/approval-auth.ts index 98b24109864..2d11cabfd56 100644 --- a/extensions/nextcloud-talk/src/approval-auth.ts +++ b/extensions/nextcloud-talk/src/approval-auth.ts @@ -1,7 +1,7 @@ import { createResolvedApproverActionAuthAdapter, resolveApprovalApprovers, -} from "openclaw/plugin-sdk/approval-runtime"; +} from "openclaw/plugin-sdk/approval-auth-runtime"; import { resolveNextcloudTalkAccount } from "./accounts.js"; import type { CoreConfig } from "./types.js"; diff --git a/extensions/signal/channel-entry.ts b/extensions/signal/channel-entry.ts new file mode 100644 index 00000000000..b31a0f0980c --- /dev/null +++ b/extensions/signal/channel-entry.ts @@ -0,0 +1,14 @@ +import { defineChannelPluginEntry } from "openclaw/plugin-sdk/channel-core"; +import { signalPlugin } from "./src/channel.js"; +import { setSignalRuntime } from "./src/runtime.js"; + +export { signalPlugin } from "./src/channel.js"; +export { setSignalRuntime } from "./src/runtime.js"; + +export default defineChannelPluginEntry({ + id: "signal", + name: "Signal", + description: "Signal channel plugin", + plugin: signalPlugin, + setRuntime: setSignalRuntime, +}); diff --git a/extensions/signal/src/approval-auth.ts b/extensions/signal/src/approval-auth.ts index df942ecede4..dcfb8e1f263 100644 --- a/extensions/signal/src/approval-auth.ts +++ b/extensions/signal/src/approval-auth.ts @@ -1,7 +1,7 @@ import { createResolvedApproverActionAuthAdapter, resolveApprovalApprovers, -} from "openclaw/plugin-sdk/approval-runtime"; +} from "openclaw/plugin-sdk/approval-auth-runtime"; import { normalizeE164 } from "openclaw/plugin-sdk/text-runtime"; import { resolveSignalAccount } from "./accounts.js"; import { normalizeSignalMessagingTarget } from "./normalize.js"; diff --git a/extensions/signal/src/rpc-context.ts b/extensions/signal/src/rpc-context.ts index 255338379d4..1a02247c09d 100644 --- a/extensions/signal/src/rpc-context.ts +++ b/extensions/signal/src/rpc-context.ts @@ -1,4 +1,3 @@ -import { loadConfig } from "openclaw/plugin-sdk/config-runtime"; import { resolveSignalAccount } from "./accounts.js"; export function resolveSignalRpcContext( @@ -7,14 +6,10 @@ export function resolveSignalRpcContext( ) { const hasBaseUrl = Boolean(opts.baseUrl?.trim()); const hasAccount = Boolean(opts.account?.trim()); - const resolvedAccount = - accountInfo || - (!hasBaseUrl || !hasAccount - ? resolveSignalAccount({ - cfg: loadConfig(), - accountId: opts.accountId, - }) - : undefined); + if ((!hasBaseUrl || !hasAccount) && !accountInfo) { + throw new Error("Signal account config is required when baseUrl or account is missing"); + } + const resolvedAccount = accountInfo; const baseUrl = opts.baseUrl?.trim() || resolvedAccount?.baseUrl; if (!baseUrl) { throw new Error("Signal base URL is required"); diff --git a/extensions/signal/src/send-reactions.ts b/extensions/signal/src/send-reactions.ts index 6b8c3791b2d..418cf913b58 100644 --- a/extensions/signal/src/send-reactions.ts +++ b/extensions/signal/src/send-reactions.ts @@ -2,7 +2,6 @@ * Signal reactions via signal-cli JSON-RPC API */ -import { loadConfig } from "openclaw/plugin-sdk/config-runtime"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime"; import { resolveSignalAccount } from "./accounts.js"; import { signalRpcRequest } from "./client.js"; @@ -31,6 +30,15 @@ type SignalReactionErrorMessages = { missingTargetAuthor: string; }; +let signalConfigRuntimePromise: + | Promise + | undefined; + +async function loadSignalConfigRuntime() { + signalConfigRuntimePromise ??= import("openclaw/plugin-sdk/config-runtime"); + return await signalConfigRuntimePromise; +} + function normalizeSignalId(raw: string): string { const trimmed = raw.trim(); if (!trimmed) { @@ -77,7 +85,7 @@ async function sendReactionSignalCore(params: { opts: SignalReactionOpts; errors: SignalReactionErrorMessages; }): Promise { - const cfg = params.opts.cfg ?? loadConfig(); + const cfg = params.opts.cfg ?? (await loadSignalConfigRuntime()).loadConfig(); const accountInfo = resolveSignalAccount({ cfg, accountId: params.opts.accountId, diff --git a/extensions/signal/src/send.ts b/extensions/signal/src/send.ts index 19777a0a269..b3a71c9061c 100644 --- a/extensions/signal/src/send.ts +++ b/extensions/signal/src/send.ts @@ -39,6 +39,28 @@ type SignalTarget = | { type: "group"; groupId: string } | { type: "username"; username: string }; +let signalConfigRuntimePromise: + | Promise + | undefined; + +async function loadSignalConfigRuntime() { + signalConfigRuntimePromise ??= import("openclaw/plugin-sdk/config-runtime"); + return await signalConfigRuntimePromise; +} + +async function resolveSignalRpcAccountInfo( + opts: Pick, +) { + if (opts.baseUrl?.trim() && opts.account?.trim()) { + return undefined; + } + const cfg = opts.cfg ?? (await loadSignalConfigRuntime()).loadConfig(); + return resolveSignalAccount({ + cfg, + accountId: opts.accountId, + }); +} + function parseTarget(raw: string): SignalTarget { let value = raw.trim(); if (!value) { @@ -203,7 +225,8 @@ export async function sendTypingSignal( to: string, opts: SignalRpcOpts & { stop?: boolean } = {}, ): Promise { - const { baseUrl, account } = resolveSignalRpcContext(opts); + const accountInfo = await resolveSignalRpcAccountInfo(opts); + const { baseUrl, account } = resolveSignalRpcContext(opts, accountInfo); const targetParams = buildTargetParams(parseTarget(to), { recipient: true, group: true, @@ -233,7 +256,8 @@ export async function sendReadReceiptSignal( if (!Number.isFinite(targetTimestamp) || targetTimestamp <= 0) { return false; } - const { baseUrl, account } = resolveSignalRpcContext(opts); + const accountInfo = await resolveSignalRpcAccountInfo(opts); + const { baseUrl, account } = resolveSignalRpcContext(opts, accountInfo); const targetParams = buildTargetParams(parseTarget(to), { recipient: true, }); diff --git a/extensions/slack/channel-entry.ts b/extensions/slack/channel-entry.ts new file mode 100644 index 00000000000..4e5a97fefd4 --- /dev/null +++ b/extensions/slack/channel-entry.ts @@ -0,0 +1,14 @@ +import { defineChannelPluginEntry } from "openclaw/plugin-sdk/channel-core"; +import { slackPlugin } from "./src/channel.js"; +import { setSlackRuntime } from "./src/runtime.js"; + +export { slackPlugin } from "./src/channel.js"; +export { setSlackRuntime } from "./src/runtime.js"; + +export default defineChannelPluginEntry({ + id: "slack", + name: "Slack", + description: "Slack channel plugin", + plugin: slackPlugin, + setRuntime: setSlackRuntime, +}); diff --git a/extensions/slack/src/approval-auth.ts b/extensions/slack/src/approval-auth.ts index 4a6a2a4789c..d1a8251bc1c 100644 --- a/extensions/slack/src/approval-auth.ts +++ b/extensions/slack/src/approval-auth.ts @@ -1,7 +1,7 @@ import { createResolvedApproverActionAuthAdapter, resolveApprovalApprovers, -} from "openclaw/plugin-sdk/approval-runtime"; +} from "openclaw/plugin-sdk/approval-auth-runtime"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime"; import { resolveSlackAccount } from "./accounts.js"; import { normalizeSlackApproverId } from "./exec-approvals.js"; diff --git a/extensions/slack/src/approval-native.ts b/extensions/slack/src/approval-native.ts index c54b830dc62..5e588e959d9 100644 --- a/extensions/slack/src/approval-native.ts +++ b/extensions/slack/src/approval-native.ts @@ -1,9 +1,11 @@ +import { + createApproverRestrictedNativeApprovalCapability, + splitChannelApprovalCapability, +} from "openclaw/plugin-sdk/approval-delivery-runtime"; import { createChannelApproverDmTargetResolver, createChannelNativeOriginTargetResolver, - createApproverRestrictedNativeApprovalCapability, - splitChannelApprovalCapability, -} from "openclaw/plugin-sdk/approval-runtime"; +} from "openclaw/plugin-sdk/approval-native-runtime"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime"; import type { ExecApprovalRequest, PluginApprovalRequest } from "openclaw/plugin-sdk/infra-runtime"; import { listSlackAccountIds } from "./accounts.js"; diff --git a/extensions/slack/src/exec-approvals.ts b/extensions/slack/src/exec-approvals.ts index 6584f027733..243cd29bc0f 100644 --- a/extensions/slack/src/exec-approvals.ts +++ b/extensions/slack/src/exec-approvals.ts @@ -1,9 +1,9 @@ +import { resolveApprovalApprovers } from "openclaw/plugin-sdk/approval-auth-runtime"; import { createChannelExecApprovalProfile, - doesApprovalRequestMatchChannelAccount, isChannelExecApprovalTargetRecipient, - resolveApprovalApprovers, -} from "openclaw/plugin-sdk/approval-runtime"; +} from "openclaw/plugin-sdk/approval-client-runtime"; +import { doesApprovalRequestMatchChannelAccount } from "openclaw/plugin-sdk/approval-native-runtime"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime"; import { resolveSlackAccount } from "./accounts.js"; diff --git a/extensions/slack/src/outbound-adapter.ts b/extensions/slack/src/outbound-adapter.ts index ccc00ef61ac..338d89a8032 100644 --- a/extensions/slack/src/outbound-adapter.ts +++ b/extensions/slack/src/outbound-adapter.ts @@ -22,9 +22,17 @@ import { parseSlackBlocksInput } from "./blocks-input.js"; import { buildSlackInteractiveBlocks, type SlackBlock } from "./blocks-render.js"; import { compileSlackInteractiveReplies } from "./interactive-replies.js"; import { SLACK_TEXT_LIMIT } from "./limits.js"; -import { sendMessageSlack, type SlackSendIdentity } from "./send.js"; +import type { SlackSendIdentity } from "./send.js"; const SLACK_MAX_BLOCKS = 50; +type SlackSendFn = typeof import("./send.runtime.js").sendMessageSlack; + +let slackSendRuntimePromise: Promise | undefined; + +async function loadSlackSendRuntime() { + slackSendRuntimePromise ??= import("./send.runtime.js"); + return await slackSendRuntimePromise; +} function resolveRenderedInteractiveBlocks( interactive?: InteractiveReply, @@ -51,7 +59,7 @@ function resolveSlackSendIdentity(identity?: OutboundIdentity): SlackSendIdentit } async function applySlackMessageSendingHooks(params: { - cfg: NonNullable[2]>["cfg"]>; + cfg: NonNullable[2]>["cfg"]>; to: string; text: string; threadTs?: string; @@ -85,7 +93,7 @@ async function applySlackMessageSendingHooks(params: { } async function sendSlackOutboundMessage(params: { - cfg: NonNullable[2]>["cfg"]>; + cfg: NonNullable[2]>["cfg"]>; to: string; text: string; mediaUrl?: string; @@ -95,7 +103,7 @@ async function sendSlackOutboundMessage(params: { }; mediaLocalRoots?: readonly string[]; mediaReadFile?: (filePath: string) => Promise; - blocks?: NonNullable[2]>["blocks"]; + blocks?: NonNullable[2]>["blocks"]; accountId?: string | null; deps?: { [channelId: string]: unknown } | null; replyToId?: string | null; @@ -103,7 +111,8 @@ async function sendSlackOutboundMessage(params: { identity?: OutboundIdentity; }) { const send = - resolveOutboundSendDep(params.deps, "slack") ?? sendMessageSlack; + resolveOutboundSendDep(params.deps, "slack") ?? + (await loadSlackSendRuntime()).sendMessageSlack; const threadTs = params.replyToId ?? (params.threadId != null ? String(params.threadId) : undefined); const hookResult = await applySlackMessageSendingHooks({ diff --git a/extensions/synology-chat/src/approval-auth.ts b/extensions/synology-chat/src/approval-auth.ts index bae9b536fb7..c8c24af15a9 100644 --- a/extensions/synology-chat/src/approval-auth.ts +++ b/extensions/synology-chat/src/approval-auth.ts @@ -2,7 +2,7 @@ import type { OpenClawConfig } from "openclaw/plugin-sdk/account-resolution"; import { createResolvedApproverActionAuthAdapter, resolveApprovalApprovers, -} from "openclaw/plugin-sdk/approval-runtime"; +} from "openclaw/plugin-sdk/approval-auth-runtime"; import { resolveAccount } from "./accounts.js"; function normalizeSynologyChatApproverId(value: string | number): string | undefined { diff --git a/extensions/telegram/src/approval-buttons.ts b/extensions/telegram/src/approval-buttons.ts index 8343b9d6bbe..03721db6d39 100644 --- a/extensions/telegram/src/approval-buttons.ts +++ b/extensions/telegram/src/approval-buttons.ts @@ -1,4 +1,4 @@ -import type { ExecApprovalReplyDecision } from "openclaw/plugin-sdk/approval-runtime"; +import type { ExecApprovalReplyDecision } from "openclaw/plugin-sdk/approval-reply-runtime"; import { sanitizeTelegramCallbackData } from "./approval-callback-data.js"; import type { TelegramInlineButtons } from "./button-types.js"; diff --git a/extensions/telegram/src/approval-native.ts b/extensions/telegram/src/approval-native.ts index d8f872f2f54..400b943bfdf 100644 --- a/extensions/telegram/src/approval-native.ts +++ b/extensions/telegram/src/approval-native.ts @@ -1,9 +1,11 @@ +import { + createApproverRestrictedNativeApprovalCapability, + splitChannelApprovalCapability, +} from "openclaw/plugin-sdk/approval-delivery-runtime"; import { createChannelApproverDmTargetResolver, createChannelNativeOriginTargetResolver, - createApproverRestrictedNativeApprovalCapability, - splitChannelApprovalCapability, -} from "openclaw/plugin-sdk/approval-runtime"; +} from "openclaw/plugin-sdk/approval-native-runtime"; import type { ChannelApprovalCapability } from "openclaw/plugin-sdk/channel-contract"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime"; import type { ExecApprovalRequest, PluginApprovalRequest } from "openclaw/plugin-sdk/infra-runtime"; diff --git a/extensions/telegram/src/exec-approval-forwarding.ts b/extensions/telegram/src/exec-approval-forwarding.ts index f42b4f55ee4..4150e381f71 100644 --- a/extensions/telegram/src/exec-approval-forwarding.ts +++ b/extensions/telegram/src/exec-approval-forwarding.ts @@ -2,9 +2,9 @@ import { buildExecApprovalPendingReplyPayload, resolveExecApprovalRequestAllowedDecisions, resolveExecApprovalCommandDisplay, - type ExecApprovalRequest, -} from "openclaw/plugin-sdk/approval-runtime"; +} from "openclaw/plugin-sdk/approval-reply-runtime"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime"; +import type { ExecApprovalRequest } from "openclaw/plugin-sdk/infra-runtime"; import { normalizeMessageChannel } from "openclaw/plugin-sdk/routing"; import { isTelegramExecApprovalClientEnabled } from "./exec-approvals.js"; diff --git a/extensions/telegram/src/exec-approvals-handler.ts b/extensions/telegram/src/exec-approvals-handler.ts index 31dddd42a26..851689f8a01 100644 --- a/extensions/telegram/src/exec-approvals-handler.ts +++ b/extensions/telegram/src/exec-approvals-handler.ts @@ -1,4 +1,4 @@ -import { buildPluginApprovalPendingReplyPayload } from "openclaw/plugin-sdk/approval-runtime"; +import { buildPluginApprovalPendingReplyPayload } from "openclaw/plugin-sdk/approval-reply-runtime"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime"; import { createChannelNativeApprovalRuntime, diff --git a/extensions/telegram/src/exec-approvals.ts b/extensions/telegram/src/exec-approvals.ts index cce8a64b5f6..436f64da77c 100644 --- a/extensions/telegram/src/exec-approvals.ts +++ b/extensions/telegram/src/exec-approvals.ts @@ -1,11 +1,11 @@ +import { resolveApprovalApprovers } from "openclaw/plugin-sdk/approval-auth-runtime"; import { createChannelExecApprovalProfile, isChannelExecApprovalClientEnabledFromConfig, isChannelExecApprovalTargetRecipient, matchesApprovalRequestFilters, - resolveApprovalRequestChannelAccountId, - resolveApprovalApprovers, -} from "openclaw/plugin-sdk/approval-runtime"; +} from "openclaw/plugin-sdk/approval-client-runtime"; +import { resolveApprovalRequestChannelAccountId } from "openclaw/plugin-sdk/approval-native-runtime"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime"; import type { TelegramExecApprovalConfig } from "openclaw/plugin-sdk/config-runtime"; import type { ExecApprovalRequest, PluginApprovalRequest } from "openclaw/plugin-sdk/infra-runtime"; diff --git a/extensions/telegram/test-support.ts b/extensions/telegram/test-support.ts index 9d02152abc3..8162357e760 100644 --- a/extensions/telegram/test-support.ts +++ b/extensions/telegram/test-support.ts @@ -1,5 +1,5 @@ import { buildDmGroupAccountAllowlistAdapter } from "openclaw/plugin-sdk/allowlist-config-edit"; -import { splitChannelApprovalCapability } from "openclaw/plugin-sdk/approval-runtime"; +import { splitChannelApprovalCapability } from "openclaw/plugin-sdk/approval-delivery-runtime"; import { getChatChannelMeta, type ChannelPlugin } from "openclaw/plugin-sdk/core"; import type { ResolvedTelegramAccount } from "./src/accounts.js"; import { resolveTelegramAccount } from "./src/accounts.js"; diff --git a/extensions/zalo/src/approval-auth.ts b/extensions/zalo/src/approval-auth.ts index c8c6fb9b71b..32ba785a859 100644 --- a/extensions/zalo/src/approval-auth.ts +++ b/extensions/zalo/src/approval-auth.ts @@ -1,7 +1,7 @@ import { createResolvedApproverActionAuthAdapter, resolveApprovalApprovers, -} from "openclaw/plugin-sdk/approval-runtime"; +} from "openclaw/plugin-sdk/approval-auth-runtime"; import { resolveZaloAccount } from "./accounts.js"; function normalizeZaloApproverId(value: string | number): string | undefined { diff --git a/package.json b/package.json index e0964f2b0cd..8135eb47093 100644 --- a/package.json +++ b/package.json @@ -95,6 +95,22 @@ "types": "./dist/plugin-sdk/approval-auth-runtime.d.ts", "default": "./dist/plugin-sdk/approval-auth-runtime.js" }, + "./plugin-sdk/approval-client-runtime": { + "types": "./dist/plugin-sdk/approval-client-runtime.d.ts", + "default": "./dist/plugin-sdk/approval-client-runtime.js" + }, + "./plugin-sdk/approval-delivery-runtime": { + "types": "./dist/plugin-sdk/approval-delivery-runtime.d.ts", + "default": "./dist/plugin-sdk/approval-delivery-runtime.js" + }, + "./plugin-sdk/approval-native-runtime": { + "types": "./dist/plugin-sdk/approval-native-runtime.d.ts", + "default": "./dist/plugin-sdk/approval-native-runtime.js" + }, + "./plugin-sdk/approval-reply-runtime": { + "types": "./dist/plugin-sdk/approval-reply-runtime.d.ts", + "default": "./dist/plugin-sdk/approval-reply-runtime.js" + }, "./plugin-sdk/approval-runtime": { "types": "./dist/plugin-sdk/approval-runtime.d.ts", "default": "./dist/plugin-sdk/approval-runtime.js" diff --git a/scripts/lib/plugin-sdk-doc-metadata.ts b/scripts/lib/plugin-sdk-doc-metadata.ts index af15c2efab6..0ac926a2e23 100644 --- a/scripts/lib/plugin-sdk-doc-metadata.ts +++ b/scripts/lib/plugin-sdk-doc-metadata.ts @@ -23,6 +23,21 @@ export const pluginSdkDocMetadata = { "approval-runtime": { category: "runtime", }, + "approval-auth-runtime": { + category: "runtime", + }, + "approval-client-runtime": { + category: "runtime", + }, + "approval-delivery-runtime": { + category: "runtime", + }, + "approval-native-runtime": { + category: "runtime", + }, + "approval-reply-runtime": { + category: "runtime", + }, "plugin-entry": { category: "core", }, diff --git a/scripts/lib/plugin-sdk-entrypoints.json b/scripts/lib/plugin-sdk-entrypoints.json index 9bb36d7035c..d1dda4dbda6 100644 --- a/scripts/lib/plugin-sdk-entrypoints.json +++ b/scripts/lib/plugin-sdk-entrypoints.json @@ -13,6 +13,10 @@ "channel-setup", "setup-tools", "approval-auth-runtime", + "approval-client-runtime", + "approval-delivery-runtime", + "approval-native-runtime", + "approval-reply-runtime", "approval-runtime", "config-runtime", "reply-runtime", diff --git a/src/channels/plugins/bundled.ts b/src/channels/plugins/bundled.ts index 1be86ae6de9..054327f40b8 100644 --- a/src/channels/plugins/bundled.ts +++ b/src/channels/plugins/bundled.ts @@ -31,6 +31,13 @@ type BundledChannelDiscoveryCandidate = { }; }; +const BUNDLED_CHANNEL_ENTRY_BASENAMES = [ + "channel-entry.ts", + "channel-entry.mts", + "channel-entry.js", + "channel-entry.mjs", +] as const; + const log = createSubsystemLogger("channels"); function resolveChannelPluginModuleEntry( @@ -171,6 +178,12 @@ function resolvePreferredBundledChannelSource( candidate: BundledChannelDiscoveryCandidate, manifest: ReturnType["plugins"][number], ): string { + for (const basename of BUNDLED_CHANNEL_ENTRY_BASENAMES) { + const preferred = resolveCompiledBundledModulePath(path.resolve(candidate.rootDir, basename)); + if (fs.existsSync(preferred)) { + return preferred; + } + } const declaredEntry = candidate.packageManifest?.extensions?.find( (entry): entry is string => typeof entry === "string" && entry.trim().length > 0, ); diff --git a/src/config/io.ts b/src/config/io.ts index 101a66df431..0cd235558af 100644 --- a/src/config/io.ts +++ b/src/config/io.ts @@ -41,6 +41,15 @@ import { applyMergePatch } from "./merge-patch.js"; import { resolveConfigPath, resolveStateDir } from "./paths.js"; import { isBlockedObjectKey } from "./prototype-keys.js"; import { applyConfigOverrides } from "./runtime-overrides.js"; +import { + clearRuntimeConfigSnapshot as clearRuntimeConfigSnapshotState, + getRuntimeConfigSnapshot as getRuntimeConfigSnapshotState, + getRuntimeConfigSnapshotRefreshHandler, + getRuntimeConfigSourceSnapshot as getRuntimeConfigSourceSnapshotState, + resetConfigRuntimeState as resetConfigRuntimeStateState, + setRuntimeConfigSnapshot as setRuntimeConfigSnapshotState, + setRuntimeConfigSnapshotRefreshHandler as setRuntimeConfigSnapshotRefreshHandlerState, +} from "./runtime-snapshot.js"; import type { OpenClawConfig, ConfigFileSnapshot, LegacyConfigIssue } from "./types.js"; import { validateConfigObjectRawWithPlugins, @@ -48,6 +57,15 @@ import { } from "./validation.js"; import { shouldWarnOnTouchedVersion } from "./version.js"; +export { + clearRuntimeConfigSnapshotState as clearRuntimeConfigSnapshot, + getRuntimeConfigSnapshotState as getRuntimeConfigSnapshot, + getRuntimeConfigSourceSnapshotState as getRuntimeConfigSourceSnapshot, + resetConfigRuntimeStateState as resetConfigRuntimeState, + setRuntimeConfigSnapshotState as setRuntimeConfigSnapshot, + setRuntimeConfigSnapshotRefreshHandlerState as setRuntimeConfigSnapshotRefreshHandler, +}; + // Re-export for backwards compatibility export { CircularIncludeError, ConfigIncludeError } from "./includes.js"; export { MissingEnvVarError } from "./env-substitution.js"; @@ -228,15 +246,6 @@ export type ReadConfigFileSnapshotForWriteResult = { writeOptions: ConfigWriteOptions; }; -export type RuntimeConfigSnapshotRefreshParams = { - sourceConfig: OpenClawConfig; -}; - -export type RuntimeConfigSnapshotRefreshHandler = { - refresh: (params: RuntimeConfigSnapshotRefreshParams) => boolean | Promise; - clearOnRefreshFailure?: () => void; -}; - export type ConfigWriteNotification = { configPath: string; sourceConfig: OpenClawConfig; @@ -2371,9 +2380,6 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) { const AUTO_OWNER_DISPLAY_SECRET_BY_PATH = new Map(); const AUTO_OWNER_DISPLAY_SECRET_PERSIST_IN_FLIGHT = new Set(); const AUTO_OWNER_DISPLAY_SECRET_PERSIST_WARNED = new Set(); -let runtimeConfigSnapshot: OpenClawConfig | null = null; -let runtimeConfigSourceSnapshot: OpenClawConfig | null = null; -let runtimeConfigSnapshotRefreshHandler: RuntimeConfigSnapshotRefreshHandler | null = null; const configWriteListeners = new Set<(event: ConfigWriteNotification) => void>(); function notifyConfigWriteListeners(event: ConfigWriteNotification): void { @@ -2399,31 +2405,6 @@ export function registerConfigWriteListener( }; } -export function setRuntimeConfigSnapshot( - config: OpenClawConfig, - sourceConfig?: OpenClawConfig, -): void { - runtimeConfigSnapshot = config; - runtimeConfigSourceSnapshot = sourceConfig ?? null; -} - -export function resetConfigRuntimeState(): void { - runtimeConfigSnapshot = null; - runtimeConfigSourceSnapshot = null; -} - -export function clearRuntimeConfigSnapshot(): void { - resetConfigRuntimeState(); -} - -export function getRuntimeConfigSnapshot(): OpenClawConfig | null { - return runtimeConfigSnapshot; -} - -export function getRuntimeConfigSourceSnapshot(): OpenClawConfig | null { - return runtimeConfigSourceSnapshot; -} - function isCompatibleTopLevelRuntimeProjectionShape(params: { runtimeSnapshot: OpenClawConfig; candidate: OpenClawConfig; @@ -2454,6 +2435,8 @@ function isCompatibleTopLevelRuntimeProjectionShape(params: { } export function projectConfigOntoRuntimeSourceSnapshot(config: OpenClawConfig): OpenClawConfig { + const runtimeConfigSnapshot = getRuntimeConfigSnapshotState(); + const runtimeConfigSourceSnapshot = getRuntimeConfigSourceSnapshotState(); if (!runtimeConfigSnapshot || !runtimeConfigSourceSnapshot) { return config; } @@ -2476,13 +2459,8 @@ export function projectConfigOntoRuntimeSourceSnapshot(config: OpenClawConfig): return coerceConfig(applyMergePatch(runtimeConfigSourceSnapshot, runtimePatch)); } -export function setRuntimeConfigSnapshotRefreshHandler( - refreshHandler: RuntimeConfigSnapshotRefreshHandler | null, -): void { - runtimeConfigSnapshotRefreshHandler = refreshHandler; -} - export function loadConfig(): OpenClawConfig { + const runtimeConfigSnapshot = getRuntimeConfigSnapshotState(); if (runtimeConfigSnapshot) { return runtimeConfigSnapshot; } @@ -2490,8 +2468,8 @@ export function loadConfig(): OpenClawConfig { // First successful load becomes the process snapshot. Long-lived runtimes // should swap this snapshot via explicit reload/watcher paths instead of // reparsing openclaw.json on hot code paths. - setRuntimeConfigSnapshot(config); - return runtimeConfigSnapshot ?? config; + setRuntimeConfigSnapshotState(config); + return getRuntimeConfigSnapshotState() ?? config; } export function getRuntimeConfig(): OpenClawConfig { @@ -2525,6 +2503,8 @@ export async function writeConfigFile( ): Promise { const io = createConfigIO(); let nextCfg = cfg; + const runtimeConfigSnapshot = getRuntimeConfigSnapshotState(); + const runtimeConfigSourceSnapshot = getRuntimeConfigSourceSnapshotState(); const hadRuntimeSnapshot = Boolean(runtimeConfigSnapshot); const hadBothSnapshots = Boolean(runtimeConfigSnapshot && runtimeConfigSourceSnapshot); if (hadBothSnapshots) { @@ -2538,20 +2518,21 @@ export async function writeConfigFile( unsetPaths: options.unsetPaths, }); const notifyCommittedWrite = () => { - if (!runtimeConfigSnapshot) { + const currentRuntimeConfig = getRuntimeConfigSnapshotState(); + if (!currentRuntimeConfig) { return; } notifyConfigWriteListeners({ configPath: io.configPath, sourceConfig: nextCfg, - runtimeConfig: runtimeConfigSnapshot, + runtimeConfig: currentRuntimeConfig, persistedHash: writeResult.persistedHash, writtenAtMs: Date.now(), }); }; // Keep the last-known-good runtime snapshot active until the specialized refresh path // succeeds, so concurrent readers do not observe unresolved SecretRefs mid-refresh. - const refreshHandler = runtimeConfigSnapshotRefreshHandler; + const refreshHandler = getRuntimeConfigSnapshotRefreshHandler(); if (refreshHandler) { try { const refreshed = await refreshHandler.refresh({ sourceConfig: nextCfg }); @@ -2576,16 +2557,16 @@ export async function writeConfigFile( // Refresh both snapshots from disk atomically so follow-up reads get normalized config and // subsequent writes still get secret-preservation merge-patch (hadBothSnapshots stays true). const fresh = io.loadConfig(); - setRuntimeConfigSnapshot(fresh, nextCfg); + setRuntimeConfigSnapshotState(fresh, nextCfg); notifyCommittedWrite(); return; } if (hadRuntimeSnapshot) { const fresh = io.loadConfig(); - setRuntimeConfigSnapshot(fresh); + setRuntimeConfigSnapshotState(fresh); notifyCommittedWrite(); return; } - setRuntimeConfigSnapshot(io.loadConfig()); + setRuntimeConfigSnapshotState(io.loadConfig()); notifyCommittedWrite(); } diff --git a/src/config/markdown-tables.ts b/src/config/markdown-tables.ts index 902e0f123b2..4872279d6e1 100644 --- a/src/config/markdown-tables.ts +++ b/src/config/markdown-tables.ts @@ -31,7 +31,12 @@ function buildDefaultTableModes(): Map { ); } -export const DEFAULT_TABLE_MODES = buildDefaultTableModes(); +let cachedDefaultTableModes: Map | null = null; + +function getDefaultTableModes(): Map { + cachedDefaultTableModes ??= buildDefaultTableModes(); + return cachedDefaultTableModes; +} const isMarkdownTableMode = (value: unknown): value is MarkdownTableMode => value === "off" || value === "bullets" || value === "code" || value === "block"; @@ -62,7 +67,7 @@ export function resolveMarkdownTableMode(params: { accountId?: string | null; }): MarkdownTableMode { const channel = normalizeChannelId(params.channel); - const defaultMode = channel ? (DEFAULT_TABLE_MODES.get(channel) ?? "code") : "code"; + const defaultMode = channel ? (getDefaultTableModes().get(channel) ?? "code") : "code"; if (!channel || !params.cfg) { return defaultMode; } diff --git a/src/config/runtime-snapshot.ts b/src/config/runtime-snapshot.ts new file mode 100644 index 00000000000..db17a78c894 --- /dev/null +++ b/src/config/runtime-snapshot.ts @@ -0,0 +1,49 @@ +import type { OpenClawConfig } from "./types.js"; + +export type RuntimeConfigSnapshotRefreshParams = { + sourceConfig: OpenClawConfig; +}; + +export type RuntimeConfigSnapshotRefreshHandler = { + refresh: (params: RuntimeConfigSnapshotRefreshParams) => boolean | Promise; + clearOnRefreshFailure?: () => void; +}; + +let runtimeConfigSnapshot: OpenClawConfig | null = null; +let runtimeConfigSourceSnapshot: OpenClawConfig | null = null; +let runtimeConfigSnapshotRefreshHandler: RuntimeConfigSnapshotRefreshHandler | null = null; + +export function setRuntimeConfigSnapshot( + config: OpenClawConfig, + sourceConfig?: OpenClawConfig, +): void { + runtimeConfigSnapshot = config; + runtimeConfigSourceSnapshot = sourceConfig ?? null; +} + +export function resetConfigRuntimeState(): void { + runtimeConfigSnapshot = null; + runtimeConfigSourceSnapshot = null; +} + +export function clearRuntimeConfigSnapshot(): void { + resetConfigRuntimeState(); +} + +export function getRuntimeConfigSnapshot(): OpenClawConfig | null { + return runtimeConfigSnapshot; +} + +export function getRuntimeConfigSourceSnapshot(): OpenClawConfig | null { + return runtimeConfigSourceSnapshot; +} + +export function setRuntimeConfigSnapshotRefreshHandler( + refreshHandler: RuntimeConfigSnapshotRefreshHandler | null, +): void { + runtimeConfigSnapshotRefreshHandler = refreshHandler; +} + +export function getRuntimeConfigSnapshotRefreshHandler(): RuntimeConfigSnapshotRefreshHandler | null { + return runtimeConfigSnapshotRefreshHandler; +} diff --git a/src/infra/approval-request-account-binding.ts b/src/infra/approval-request-account-binding.ts new file mode 100644 index 00000000000..4d616e6818c --- /dev/null +++ b/src/infra/approval-request-account-binding.ts @@ -0,0 +1,118 @@ +import type { OpenClawConfig } from "../config/config.js"; +import { resolveStorePath } from "../config/sessions/paths.js"; +import { loadSessionStore } from "../config/sessions/store-load.js"; +import { normalizeOptionalAccountId } from "../routing/account-id.js"; +import { parseAgentSessionKey } from "../routing/session-key.js"; +import { normalizeMessageChannel } from "../utils/message-channel.js"; +import type { ExecApprovalRequest } from "./exec-approvals.js"; +import type { PluginApprovalRequest } from "./plugin-approvals.js"; + +type ApprovalRequestLike = ExecApprovalRequest | PluginApprovalRequest; + +type ApprovalRequestSessionBinding = { + channel?: string; + accountId?: string; +}; + +function normalizeOptionalString(value?: string | null): string | undefined { + const normalized = value?.trim(); + return normalized ? normalized : undefined; +} + +function normalizeOptionalChannel(value?: string | null): string | undefined { + return normalizeMessageChannel(value); +} + +function resolvePersistedApprovalRequestSessionBinding(params: { + cfg: OpenClawConfig; + request: ApprovalRequestLike; +}): ApprovalRequestSessionBinding | null { + const sessionKey = normalizeOptionalString(params.request.request.sessionKey); + if (!sessionKey) { + return null; + } + const parsed = parseAgentSessionKey(sessionKey); + const agentId = parsed?.agentId ?? params.request.request.agentId ?? "main"; + const storePath = resolveStorePath(params.cfg.session?.store, { agentId }); + const store = loadSessionStore(storePath); + const entry = store[sessionKey]; + if (!entry) { + return null; + } + const channel = normalizeOptionalChannel(entry.origin?.provider ?? entry.lastChannel); + const accountId = normalizeOptionalAccountId(entry.origin?.accountId ?? entry.lastAccountId); + return channel || accountId ? { channel, accountId } : null; +} + +export function resolveApprovalRequestAccountId(params: { + cfg: OpenClawConfig; + request: ApprovalRequestLike; + channel?: string | null; +}): string | null { + const expectedChannel = normalizeOptionalChannel(params.channel); + const turnSourceChannel = normalizeOptionalChannel(params.request.request.turnSourceChannel); + if (expectedChannel && turnSourceChannel && turnSourceChannel !== expectedChannel) { + return null; + } + + const turnSourceAccountId = normalizeOptionalAccountId( + params.request.request.turnSourceAccountId, + ); + if (turnSourceAccountId) { + return turnSourceAccountId; + } + + const sessionBinding = resolvePersistedApprovalRequestSessionBinding(params); + const sessionChannel = sessionBinding?.channel; + if (expectedChannel && sessionChannel && sessionChannel !== expectedChannel) { + return null; + } + + return sessionBinding?.accountId ?? null; +} + +export function resolveApprovalRequestChannelAccountId(params: { + cfg: OpenClawConfig; + request: ApprovalRequestLike; + channel: string; +}): string | null { + const expectedChannel = normalizeOptionalChannel(params.channel); + if (!expectedChannel) { + return null; + } + return resolveApprovalRequestAccountId(params); +} + +export function doesApprovalRequestMatchChannelAccount(params: { + cfg: OpenClawConfig; + request: ApprovalRequestLike; + channel: string; + accountId?: string | null; +}): boolean { + const expectedChannel = normalizeOptionalChannel(params.channel); + if (!expectedChannel) { + return false; + } + + const turnSourceChannel = normalizeOptionalChannel(params.request.request.turnSourceChannel); + if (turnSourceChannel && turnSourceChannel !== expectedChannel) { + return false; + } + + const turnSourceAccountId = normalizeOptionalAccountId( + params.request.request.turnSourceAccountId, + ); + const expectedAccountId = normalizeOptionalAccountId(params.accountId); + if (turnSourceAccountId) { + return !expectedAccountId || expectedAccountId === turnSourceAccountId; + } + + const sessionBinding = resolvePersistedApprovalRequestSessionBinding(params); + const sessionChannel = sessionBinding?.channel; + if (sessionChannel && sessionChannel !== expectedChannel) { + return false; + } + + const boundAccountId = sessionBinding?.accountId; + return !expectedAccountId || !boundAccountId || expectedAccountId === boundAccountId; +} diff --git a/src/infra/exec-approval-session-target.ts b/src/infra/exec-approval-session-target.ts index 97b461147ab..b450d1f39a8 100644 --- a/src/infra/exec-approval-session-target.ts +++ b/src/infra/exec-approval-session-target.ts @@ -1,8 +1,9 @@ import type { OpenClawConfig } from "../config/config.js"; -import { loadSessionStore, resolveStorePath } from "../config/sessions.js"; -import { normalizeOptionalAccountId } from "../routing/account-id.js"; +import { resolveStorePath } from "../config/sessions/paths.js"; +import { loadSessionStore } from "../config/sessions/store-load.js"; import { parseAgentSessionKey } from "../routing/session-key.js"; import { normalizeMessageChannel } from "../utils/message-channel.js"; +import { doesApprovalRequestMatchChannelAccount } from "./approval-request-account-binding.js"; import type { ExecApprovalRequest } from "./exec-approvals.js"; import { resolveSessionDeliveryTarget } from "./outbound/targets.js"; import type { PluginApprovalRequest } from "./plugin-approvals.js"; @@ -14,11 +15,6 @@ export type ExecApprovalSessionTarget = { threadId?: number; }; -type ApprovalRequestSessionBinding = { - channel?: string; - accountId?: string; -}; - type ApprovalRequestLike = ExecApprovalRequest | PluginApprovalRequest; type ApprovalRequestOriginTargetResolver = { cfg: OpenClawConfig; @@ -115,28 +111,6 @@ export function resolveExecApprovalSessionTarget(params: { }; } -function resolvePersistedApprovalRequestSessionBinding(params: { - cfg: OpenClawConfig; - request: ApprovalRequestLike; -}): ApprovalRequestSessionBinding | null { - const sessionKey = normalizeOptionalString(params.request.request.sessionKey); - if (!sessionKey) { - return null; - } - const parsed = parseAgentSessionKey(sessionKey); - const agentId = parsed?.agentId ?? params.request.request.agentId ?? "main"; - const storePath = resolveStorePath(params.cfg.session?.store, { agentId }); - const store = loadSessionStore(storePath); - const entry = store[sessionKey]; - if (!entry) { - return null; - } - return { - channel: normalizeOptionalChannel(entry.origin?.provider ?? entry.lastChannel), - accountId: normalizeOptionalAccountId(entry.origin?.accountId ?? entry.lastAccountId), - }; -} - export function resolveApprovalRequestSessionTarget(params: { cfg: OpenClawConfig; request: ApprovalRequestLike; @@ -163,117 +137,6 @@ function resolveApprovalRequestStoredSessionTarget(params: { }); } -// Account scoping uses the persisted same-channel binding first. The generic -// session target only backfills legacy sessions that never stored `origin.*`. -function resolveApprovalRequestAccountBinding(params: { - cfg: OpenClawConfig; - request: ApprovalRequestLike; - sessionTarget?: ExecApprovalSessionTarget | null; -}): ApprovalRequestSessionBinding | null { - const sessionBinding = resolvePersistedApprovalRequestSessionBinding(params); - const channel = normalizeOptionalChannel( - sessionBinding?.channel ?? params.sessionTarget?.channel, - ); - const accountId = normalizeOptionalAccountId( - sessionBinding?.accountId ?? params.sessionTarget?.accountId, - ); - return channel || accountId ? { channel, accountId } : null; -} - -export function resolveApprovalRequestAccountId(params: { - cfg: OpenClawConfig; - request: ApprovalRequestLike; - channel?: string | null; -}): string | null { - const expectedChannel = normalizeOptionalChannel(params.channel); - const turnSourceChannel = normalizeOptionalChannel(params.request.request.turnSourceChannel); - if (expectedChannel && turnSourceChannel && turnSourceChannel !== expectedChannel) { - return null; - } - - const turnSourceAccountId = normalizeOptionalAccountId( - params.request.request.turnSourceAccountId, - ); - if (turnSourceAccountId) { - return turnSourceAccountId; - } - - const sessionBinding = resolveApprovalRequestAccountBinding({ - ...params, - sessionTarget: resolveApprovalRequestSessionTarget(params), - }); - const sessionChannel = sessionBinding?.channel; - if (expectedChannel && sessionChannel && sessionChannel !== expectedChannel) { - return null; - } - - return sessionBinding?.accountId ?? null; -} - -export function resolveApprovalRequestChannelAccountId(params: { - cfg: OpenClawConfig; - request: ApprovalRequestLike; - channel: string; -}): string | null { - const expectedChannel = normalizeOptionalChannel(params.channel); - if (!expectedChannel) { - return null; - } - - const turnSourceChannel = normalizeOptionalChannel(params.request.request.turnSourceChannel); - if (!turnSourceChannel || turnSourceChannel === expectedChannel) { - return resolveApprovalRequestAccountId(params); - } - - const sessionBinding = resolveApprovalRequestAccountBinding({ - ...params, - sessionTarget: resolveApprovalRequestStoredSessionTarget(params), - }); - const sessionChannel = sessionBinding?.channel; - if (sessionChannel && sessionChannel !== expectedChannel) { - return null; - } - - return sessionBinding?.accountId ?? null; -} - -export function doesApprovalRequestMatchChannelAccount(params: { - cfg: OpenClawConfig; - request: ApprovalRequestLike; - channel: string; - accountId?: string | null; -}): boolean { - const expectedChannel = normalizeOptionalChannel(params.channel); - if (!expectedChannel) { - return false; - } - - const turnSourceChannel = normalizeOptionalChannel(params.request.request.turnSourceChannel); - if (turnSourceChannel && turnSourceChannel !== expectedChannel) { - return false; - } - - const turnSourceAccountId = normalizeOptionalAccountId( - params.request.request.turnSourceAccountId, - ); - const expectedAccountId = normalizeOptionalAccountId(params.accountId); - if (turnSourceAccountId) { - return !expectedAccountId || expectedAccountId === turnSourceAccountId; - } - - const sessionBinding = resolveApprovalRequestAccountBinding({ - ...params, - sessionTarget: resolveApprovalRequestSessionTarget(params), - }); - const sessionChannel = sessionBinding?.channel; - if (sessionChannel && sessionChannel !== expectedChannel) { - return false; - } - - const boundAccountId = sessionBinding?.accountId; - return !expectedAccountId || !boundAccountId || expectedAccountId === boundAccountId; -} - export function resolveApprovalRequestOriginTarget( params: ApprovalRequestOriginTargetResolver, ): TTarget | null { diff --git a/src/plugin-sdk/approval-client-helpers.ts b/src/plugin-sdk/approval-client-helpers.ts index ab111597273..19266f6e484 100644 --- a/src/plugin-sdk/approval-client-helpers.ts +++ b/src/plugin-sdk/approval-client-helpers.ts @@ -36,6 +36,8 @@ function isApprovalTargetsMode(cfg: OpenClawConfig): boolean { return execApprovals.mode === "targets" || execApprovals.mode === "both"; } +export { getExecApprovalReplyMetadata, matchesApprovalRequestFilters }; + export function isChannelExecApprovalClientEnabledFromConfig(params: { enabled?: ChannelExecApprovalEnableMode; approverCount: number; diff --git a/src/plugin-sdk/approval-client-runtime.ts b/src/plugin-sdk/approval-client-runtime.ts new file mode 100644 index 00000000000..62cc2672a29 --- /dev/null +++ b/src/plugin-sdk/approval-client-runtime.ts @@ -0,0 +1,7 @@ +export { + createChannelExecApprovalProfile, + getExecApprovalReplyMetadata, + isChannelExecApprovalClientEnabledFromConfig, + isChannelExecApprovalTargetRecipient, + matchesApprovalRequestFilters, +} from "./approval-client-helpers.js"; diff --git a/src/plugin-sdk/approval-delivery-runtime.ts b/src/plugin-sdk/approval-delivery-runtime.ts new file mode 100644 index 00000000000..4eb4c0cc90f --- /dev/null +++ b/src/plugin-sdk/approval-delivery-runtime.ts @@ -0,0 +1,6 @@ +export { + createApproverRestrictedNativeApprovalAdapter, + createApproverRestrictedNativeApprovalCapability, + createChannelApprovalCapability, + splitChannelApprovalCapability, +} from "./approval-delivery-helpers.js"; diff --git a/src/plugin-sdk/approval-native-runtime.ts b/src/plugin-sdk/approval-native-runtime.ts new file mode 100644 index 00000000000..c77208ed0d0 --- /dev/null +++ b/src/plugin-sdk/approval-native-runtime.ts @@ -0,0 +1,15 @@ +export { + createChannelApproverDmTargetResolver, + createChannelNativeOriginTargetResolver, +} from "./approval-native-helpers.js"; +export { + resolveApprovalRequestOriginTarget, + resolveApprovalRequestSessionTarget, + resolveExecApprovalSessionTarget, + type ExecApprovalSessionTarget, +} from "../infra/exec-approval-session-target.js"; +export { + doesApprovalRequestMatchChannelAccount, + resolveApprovalRequestAccountId, + resolveApprovalRequestChannelAccountId, +} from "../infra/approval-request-account-binding.js"; diff --git a/src/plugin-sdk/approval-reply-runtime.ts b/src/plugin-sdk/approval-reply-runtime.ts new file mode 100644 index 00000000000..d2e57e99ccc --- /dev/null +++ b/src/plugin-sdk/approval-reply-runtime.ts @@ -0,0 +1,15 @@ +export { + buildExecApprovalPendingReplyPayload, + getExecApprovalApproverDmNoticeText, + getExecApprovalReplyMetadata, + type ExecApprovalPendingReplyParams, + type ExecApprovalReplyDecision, + type ExecApprovalReplyMetadata, +} from "../infra/exec-approval-reply.js"; +export { resolveExecApprovalCommandDisplay } from "../infra/exec-approval-command-display.js"; +export { + resolveExecApprovalAllowedDecisions, + resolveExecApprovalRequestAllowedDecisions, + type ExecApprovalDecision, +} from "../infra/exec-approvals.js"; +export { buildPluginApprovalPendingReplyPayload } from "./approval-renderers.js"; diff --git a/src/plugin-sdk/approval-runtime.ts b/src/plugin-sdk/approval-runtime.ts index f85bd2b9b5c..7c588c03fd2 100644 --- a/src/plugin-sdk/approval-runtime.ts +++ b/src/plugin-sdk/approval-runtime.ts @@ -24,14 +24,16 @@ export { createChannelNativeOriginTargetResolver, } from "./approval-native-helpers.js"; export { - doesApprovalRequestMatchChannelAccount, resolveApprovalRequestOriginTarget, - resolveApprovalRequestAccountId, - resolveApprovalRequestChannelAccountId, resolveApprovalRequestSessionTarget, resolveExecApprovalSessionTarget, type ExecApprovalSessionTarget, } from "../infra/exec-approval-session-target.js"; +export { + doesApprovalRequestMatchChannelAccount, + resolveApprovalRequestAccountId, + resolveApprovalRequestChannelAccountId, +} from "../infra/approval-request-account-binding.js"; export { buildPluginApprovalExpiredMessage, buildPluginApprovalRequestMessage, diff --git a/src/plugin-sdk/core.ts b/src/plugin-sdk/core.ts index fc7b0c0af9a..27b3e722124 100644 --- a/src/plugin-sdk/core.ts +++ b/src/plugin-sdk/core.ts @@ -443,21 +443,15 @@ export function defineChannelPluginEntry({ registerCliMetadata, registerFull, }: DefineChannelPluginEntryOptions): DefinedChannelPluginEntry { - let resolvedConfigSchema: ChannelEntryConfigSchema | undefined; - const getConfigSchema = (): ChannelEntryConfigSchema => { - resolvedConfigSchema ??= - typeof configSchema === "function" - ? configSchema() - : ((configSchema ?? emptyChannelConfigSchema()) as ChannelEntryConfigSchema); - return resolvedConfigSchema; - }; + const resolvedConfigSchema: ChannelEntryConfigSchema = + typeof configSchema === "function" + ? configSchema() + : ((configSchema ?? emptyChannelConfigSchema()) as ChannelEntryConfigSchema); const entry = { id, name, description, - get configSchema() { - return getConfigSchema(); - }, + configSchema: resolvedConfigSchema, register(api: OpenClawPluginApi) { if (api.registrationMode === "cli-metadata") { registerCliMetadata?.(api); diff --git a/src/plugin-sdk/facade-runtime.ts b/src/plugin-sdk/facade-runtime.ts index 4a4e424866b..f79ac4a59d9 100644 --- a/src/plugin-sdk/facade-runtime.ts +++ b/src/plugin-sdk/facade-runtime.ts @@ -2,8 +2,10 @@ import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { createJiti } from "jiti"; -import { loadConfig, type OpenClawConfig } from "../config/config.js"; +import JSON5 from "json5"; +import { resolveConfigPath } from "../config/paths.js"; import { applyPluginAutoEnable } from "../config/plugin-auto-enable.js"; +import type { OpenClawConfig } from "../config/types.js"; import { openBoundaryFileSync } from "../infra/boundary-file-read.js"; import { resolveBundledPluginsDir } from "../plugins/bundled-dir.js"; import { resolveBundledPluginPublicSurfacePath } from "../plugins/bundled-plugin-metadata.js"; @@ -136,8 +138,15 @@ function getJiti(modulePath: string) { function readFacadeBoundaryConfigSafely(): OpenClawConfig { try { - const config = loadConfig(); - return config && typeof config === "object" ? config : EMPTY_FACADE_BOUNDARY_CONFIG; + const configPath = resolveConfigPath(); + if (!fs.existsSync(configPath)) { + return EMPTY_FACADE_BOUNDARY_CONFIG; + } + const raw = fs.readFileSync(configPath, "utf8"); + const parsed = JSON5.parse(raw); + return parsed && typeof parsed === "object" + ? (parsed as OpenClawConfig) + : EMPTY_FACADE_BOUNDARY_CONFIG; } catch { return EMPTY_FACADE_BOUNDARY_CONFIG; } diff --git a/src/plugin-sdk/runtime-config-snapshot.ts b/src/plugin-sdk/runtime-config-snapshot.ts index a00e7de1e85..6016f0ffe7e 100644 --- a/src/plugin-sdk/runtime-config-snapshot.ts +++ b/src/plugin-sdk/runtime-config-snapshot.ts @@ -1,2 +1,2 @@ -export { getRuntimeConfigSnapshot } from "../config/io.js"; +export { getRuntimeConfigSnapshot } from "../config/runtime-snapshot.js"; export type { OpenClawConfig } from "../config/types.js";