From 3a3fdf19203b78326f5ffb913f95921dc706b939 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 4 Apr 2026 00:24:47 +0100 Subject: [PATCH] fix(ci): restore plugin contract surfaces --- extensions/discord/src/security-contract.ts | 3 + extensions/whatsapp/contract-api.ts | 3 + extensions/zalo/src/secret-contract.ts | 108 ++++++++++++++++++++ src/auto-reply/reply/stage-sandbox-media.ts | 2 +- src/infra/retry-policy.test.ts | 2 +- src/plugin-sdk/channel-contract.ts | 1 + 6 files changed, 117 insertions(+), 2 deletions(-) create mode 100644 extensions/zalo/src/secret-contract.ts diff --git a/extensions/discord/src/security-contract.ts b/extensions/discord/src/security-contract.ts index ad56803fbfc..952ee0478d2 100644 --- a/extensions/discord/src/security-contract.ts +++ b/extensions/discord/src/security-contract.ts @@ -15,6 +15,9 @@ export const unsupportedSecretRefSurfacePatterns = [ export function collectUnsupportedSecretRefConfigCandidates( raw: unknown, ): UnsupportedSecretRefConfigCandidate[] { + if (!isRecord(raw)) { + return []; + } if (!isRecord(raw.channels) || !isRecord(raw.channels.discord)) { return []; } diff --git a/extensions/whatsapp/contract-api.ts b/extensions/whatsapp/contract-api.ts index 3cd1e8fde37..0c7c76a67d6 100644 --- a/extensions/whatsapp/contract-api.ts +++ b/extensions/whatsapp/contract-api.ts @@ -22,6 +22,9 @@ export { __testing as whatsappAccessControlTesting } from "./src/inbound/access- export function collectUnsupportedSecretRefConfigCandidates( raw: unknown, ): UnsupportedSecretRefConfigCandidate[] { + if (!isRecord(raw)) { + return []; + } if (!isRecord(raw.channels) || !isRecord(raw.channels.whatsapp)) { return []; } diff --git a/extensions/zalo/src/secret-contract.ts b/extensions/zalo/src/secret-contract.ts new file mode 100644 index 00000000000..60048075429 --- /dev/null +++ b/extensions/zalo/src/secret-contract.ts @@ -0,0 +1,108 @@ +import { + collectConditionalChannelFieldAssignments, + getChannelSurface, + hasOwnProperty, + type ResolverContext, + type SecretDefaults, + type SecretTargetRegistryEntry, +} from "openclaw/plugin-sdk/security-runtime"; + +export const secretTargetRegistryEntries = [ + { + id: "channels.zalo.accounts.*.botToken", + targetType: "channels.zalo.accounts.*.botToken", + configFile: "openclaw.json", + pathPattern: "channels.zalo.accounts.*.botToken", + secretShape: "secret_input", + expectedResolvedValue: "string", + includeInPlan: true, + includeInConfigure: true, + includeInAudit: true, + }, + { + id: "channels.zalo.accounts.*.webhookSecret", + targetType: "channels.zalo.accounts.*.webhookSecret", + configFile: "openclaw.json", + pathPattern: "channels.zalo.accounts.*.webhookSecret", + secretShape: "secret_input", + expectedResolvedValue: "string", + includeInPlan: true, + includeInConfigure: true, + includeInAudit: true, + }, + { + id: "channels.zalo.botToken", + targetType: "channels.zalo.botToken", + configFile: "openclaw.json", + pathPattern: "channels.zalo.botToken", + secretShape: "secret_input", + expectedResolvedValue: "string", + includeInPlan: true, + includeInConfigure: true, + includeInAudit: true, + }, + { + id: "channels.zalo.webhookSecret", + targetType: "channels.zalo.webhookSecret", + configFile: "openclaw.json", + pathPattern: "channels.zalo.webhookSecret", + secretShape: "secret_input", + expectedResolvedValue: "string", + includeInPlan: true, + includeInConfigure: true, + includeInAudit: true, + }, +] satisfies SecretTargetRegistryEntry[]; + +export function collectRuntimeConfigAssignments(params: { + config: { channels?: Record }; + defaults: SecretDefaults | undefined; + context: ResolverContext; +}): void { + const resolved = getChannelSurface(params.config, "zalo"); + if (!resolved) { + return; + } + const { channel: zalo, surface } = resolved; + const baseTokenFile = typeof zalo.tokenFile === "string" ? zalo.tokenFile.trim() : ""; + const accountTokenFile = (account: Record) => + typeof account.tokenFile === "string" ? account.tokenFile.trim() : ""; + collectConditionalChannelFieldAssignments({ + channelKey: "zalo", + field: "botToken", + channel: zalo, + surface, + defaults: params.defaults, + context: params.context, + topLevelActiveWithoutAccounts: baseTokenFile.length === 0, + topLevelInheritedAccountActive: ({ account, enabled }) => + enabled && !hasOwnProperty(account, "botToken") && accountTokenFile(account).length === 0, + accountActive: ({ account, enabled }) => enabled && accountTokenFile(account).length === 0, + topInactiveReason: + "no enabled Zalo surface inherits this top-level botToken (tokenFile is configured).", + accountInactiveReason: "Zalo account is disabled or tokenFile is configured.", + }); + const baseWebhookUrl = typeof zalo.webhookUrl === "string" ? zalo.webhookUrl.trim() : ""; + const accountWebhookUrl = (account: Record) => + hasOwnProperty(account, "webhookUrl") + ? typeof account.webhookUrl === "string" + ? account.webhookUrl.trim() + : "" + : baseWebhookUrl; + collectConditionalChannelFieldAssignments({ + channelKey: "zalo", + field: "webhookSecret", + channel: zalo, + surface, + defaults: params.defaults, + context: params.context, + topLevelActiveWithoutAccounts: baseWebhookUrl.length > 0, + topLevelInheritedAccountActive: ({ account, enabled }) => + enabled && !hasOwnProperty(account, "webhookSecret") && accountWebhookUrl(account).length > 0, + accountActive: ({ account, enabled }) => enabled && accountWebhookUrl(account).length > 0, + topInactiveReason: + "no enabled Zalo webhook surface inherits this top-level webhookSecret (webhook mode is not active).", + accountInactiveReason: + "Zalo account is disabled or webhook mode is not active for this account.", + }); +} diff --git a/src/auto-reply/reply/stage-sandbox-media.ts b/src/auto-reply/reply/stage-sandbox-media.ts index 5feca6698c6..53bf773620d 100644 --- a/src/auto-reply/reply/stage-sandbox-media.ts +++ b/src/auto-reply/reply/stage-sandbox-media.ts @@ -182,7 +182,7 @@ function resolveAbsolutePath(value: string): string | null { async function isAllowedSourcePath(params: { source: string; mediaRemoteHost?: string; - remoteAttachmentRoots: string[]; + remoteAttachmentRoots: readonly string[]; }): Promise { if (params.mediaRemoteHost) { if ( diff --git a/src/infra/retry-policy.test.ts b/src/infra/retry-policy.test.ts index 7ba5e9cb3e3..04a52a281e1 100644 --- a/src/infra/retry-policy.test.ts +++ b/src/infra/retry-policy.test.ts @@ -163,7 +163,7 @@ describe("createChannelApiRetryRunner", () => { it("honors nested retry_after hints before retrying", async () => { vi.useFakeTimers(); - const runner = createTelegramRetryRunner({ + const runner = createChannelApiRetryRunner({ retry: { attempts: 2, minDelayMs: 0, maxDelayMs: 1_000, jitter: 0 }, }); const fn = vi diff --git a/src/plugin-sdk/channel-contract.ts b/src/plugin-sdk/channel-contract.ts index 5c6c76df24d..e2fec3be03d 100644 --- a/src/plugin-sdk/channel-contract.ts +++ b/src/plugin-sdk/channel-contract.ts @@ -30,6 +30,7 @@ export type { ChannelDoctorAdapter, ChannelDoctorConfigMutation, ChannelDoctorEmptyAllowlistAccountContext, + ChannelDoctorLegacyConfigRule, ChannelDoctorSequenceResult, ChannelGatewayContext, ChannelOutboundAdapter,