fix(whatsapp): remove exposeErrorText config (#74642)

* fix(whatsapp): remove exposeErrorText config

* fix(whatsapp): mark internal system events trusted
This commit is contained in:
Marcus Castro
2026-04-29 20:03:58 -03:00
committed by GitHub
parent 426107d2f8
commit 4cba08df01
15 changed files with 13 additions and 149 deletions

View File

@@ -230,7 +230,7 @@ Docs: https://docs.openclaw.ai
- Channels/WhatsApp: detect explicit group `@mentions` again when the bot's own E.164 is in `allowFrom`, so shared-number setups no longer skip group pings that directly mention the bot. Fixes #49317. (#73453) Thanks @juan-flores077.
- WhatsApp/reliability: publish real transport-liveness into WhatsApp channel status and force earlier reconnects on silent transport stalls, so quiet healthy sessions stay connected while wedged sockets recover before the later remote 408 path. (#72656) Thanks @Sathvik-1007.
- Core/channels: tighten selected runtime, media, and plugin edge-case handling while preserving existing behavior. Thanks @jesse-merhi.
- Channels/WhatsApp: strip leaked plural tool-call XML wrappers on every WhatsApp-visible outbound path and allow `channels.whatsapp.exposeErrorText` to suppress visible error text per channel or account. (#71830) Thanks @rubencu.
- Channels/WhatsApp: strip leaked plural tool-call XML wrappers on every WhatsApp-visible outbound path and keep channel error payloads out of WhatsApp chats. (#71830) Thanks @rubencu.
- Agents/embedded-runner: inject the resolved OAuth bearer (and forward the run abort signal) on the boundary-aware embedded stream fallback so models that route through `openai-codex-responses` and other boundary-aware transports stop failing with `401 Unauthorized: Missing bearer or basic authentication in header`. Fixes #73559. (#73588) Thanks @openperf.
- Telegram/gateway: bound outbound Bot API calls and cache bundled plugin alias lookup so slow Telegram sends or WSL2 filesystem scans no longer wedge gateway replies. (#74210) Thanks @obviyus.
- Configure/GitHub Copilot: reuse existing Copilot auth during configure and show the provider's manifest model catalog in the model picker. (#74276) Thanks @obviyus.

View File

@@ -1,4 +1,4 @@
bfd8b3ddcac047e486e9c43fdedc002cb9bf87b659f6563f9f11c850c5b2aaef config-baseline.json
2af6bef21f530dc64e0379f7631bed410aee1d5c86604ef9fb149f546cfcb0e8 config-baseline.json
8d75df355b7f6e44b9c2f195d9df86130beb697e26061469df7d60b7e8a2f204 config-baseline.core.json
9f5fad66a49fa618d64a963470aa69fed9fe4b4639cc4321f9ec04bfb2f8aa50 config-baseline.channel.json
fab66aa304db5697e87259165ad261006719eb6e6cdbd25f957fcba2b7b324e9 config-baseline.channel.json
c4231c2194206547af8ad94342dc00aadb734f43cb49cc79d4c46bdbb80c3f95 config-baseline.plugin.json

View File

@@ -398,22 +398,6 @@ When the linked self number is also present in `allowFrom`, WhatsApp self-chat s
</Accordion>
</AccordionGroup>
## Error visibility
`channels.whatsapp.exposeErrorText` controls whether agent/provider error text is delivered back into WhatsApp. The default is `true`. Set it to `false` to keep failures quiet on WhatsApp while preserving other channel behavior.
```json5
{
channels: {
whatsapp: {
exposeErrorText: false,
},
},
}
```
Per-account overrides use `channels.whatsapp.accounts.<id>.exposeErrorText`.
## Reply quoting
WhatsApp supports native reply quoting, where outbound replies visibly quote the inbound message. Control it with `channels.whatsapp.replyToMode`.
@@ -681,7 +665,7 @@ Primary reference:
High-signal WhatsApp fields:
- access: `dmPolicy`, `allowFrom`, `groupPolicy`, `groupAllowFrom`, `groups`
- delivery: `textChunkLimit`, `chunkMode`, `mediaMaxMb`, `sendReadReceipts`, `ackReaction`, `reactionLevel`, `exposeErrorText`
- delivery: `textChunkLimit`, `chunkMode`, `mediaMaxMb`, `sendReadReceipts`, `ackReaction`, `reactionLevel`
- multi-account: `accounts.<id>.enabled`, `accounts.<id>.authDir`, account-level overrides
- operations: `configWrites`, `debounceMs`, `web.enabled`, `web.heartbeatSeconds`, `web.reconnect.*`, `web.whatsapp.*`
- session behavior: `session.dmScope`, `historyLimit`, `dmHistoryLimit`, `dms.<id>.historyLimit`

View File

@@ -45,7 +45,6 @@ export type ResolvedWhatsAppAccount = {
direct?: WhatsAppAccountConfig["direct"];
debounceMs?: number;
replyToMode?: ReplyToMode;
exposeErrorText?: boolean;
};
export const DEFAULT_WHATSAPP_MEDIA_MAX_MB = 50;
@@ -157,7 +156,6 @@ export function resolveWhatsAppAccount(params: {
direct: merged.direct,
debounceMs: merged.debounceMs,
replyToMode: merged.replyToMode,
exposeErrorText: merged.exposeErrorText,
};
}

View File

@@ -176,7 +176,6 @@ export async function monitorWebChannel(
mediaMaxMb: account.mediaMaxMb,
blockStreaming: account.blockStreaming,
groups: account.groups,
exposeErrorText: account.exposeErrorText,
},
},
} satisfies ReturnType<typeof getRuntimeConfig>;
@@ -444,6 +443,7 @@ export async function monitorWebChannel(
});
enqueueSystemEvent(`WhatsApp gateway connected${selfE164 ? ` as ${selfE164}` : ""}.`, {
sessionKey: connectRoute.sessionKey,
trusted: true,
});
const normalizedAccountId = normalizeReconnectAccountId(account.accountId);
@@ -503,6 +503,7 @@ export async function monitorWebChannel(
`WhatsApp gateway disconnected (status ${decision.normalized.statusLabel})`,
{
sessionKey: connectRoute.sessionKey,
trusted: true,
},
);

View File

@@ -522,43 +522,11 @@ describe("whatsapp inbound dispatch", () => {
expect(rememberSentText).not.toHaveBeenCalled();
});
it("suppresses error payload text when exposeErrorText is false", async () => {
it("suppresses error payload text", async () => {
const deliverReply = vi.fn(async () => undefined);
const rememberSentText = vi.fn();
await dispatchBufferedReply({
cfg: { channels: { whatsapp: { exposeErrorText: false } } } as never,
deliverReply,
rememberSentText,
});
const deliver = getCapturedDeliver();
expect(deliver).toBeTypeOf("function");
await deliver?.({ text: "provider exploded", isError: true }, { kind: "final" });
expect(deliverReply).not.toHaveBeenCalled();
expect(rememberSentText).not.toHaveBeenCalled();
});
it("honors account-level exposeErrorText overrides for error payloads", async () => {
const deliverReply = vi.fn(async () => undefined);
const rememberSentText = vi.fn();
await dispatchBufferedReply({
cfg: {
channels: {
whatsapp: {
accounts: {
work: { exposeErrorText: false },
},
},
},
} as never,
deliverReply,
rememberSentText,
route: makeRoute({ accountId: "work" }),
});
await dispatchBufferedReply({ deliverReply, rememberSentText });
const deliver = getCapturedDeliver();
expect(deliver).toBeTypeOf("function");

View File

@@ -1,4 +1,3 @@
import { resolveMergedWhatsAppAccountConfig } from "../../account-config.js";
import {
type DeliverableWhatsAppOutboundPayload,
normalizeWhatsAppOutboundPayload,
@@ -89,12 +88,11 @@ function resolveWhatsAppDisableBlockStreaming(cfg: ReturnType<LoadConfigFn>): bo
function resolveWhatsAppDeliverablePayload(
payload: ReplyPayload,
info: { kind: ReplyLifecycleKind },
options?: { exposeErrorText?: boolean },
): ReplyPayload | null {
if (payload.isReasoning === true || payload.isCompactionNotice === true) {
return null;
}
if (payload.isError === true && options?.exposeErrorText === false) {
if (payload.isError === true) {
return null;
}
if (info.kind === "tool") {
@@ -314,9 +312,6 @@ export async function dispatchWhatsAppBufferedReply(params: {
});
const mediaLocalRoots = getAgentScopedMediaLocalRoots(params.cfg, params.route.agentId);
const disableBlockStreaming = resolveWhatsAppDisableBlockStreaming(params.cfg);
const exposeErrorText =
resolveMergedWhatsAppAccountConfig({ cfg: params.cfg, accountId: params.route.accountId })
.exposeErrorText !== false;
let didSendReply = false;
let didLogHeartbeatStrip = false;
@@ -333,9 +328,7 @@ export async function dispatchWhatsAppBufferedReply(params: {
}
},
deliver: async (payload: ReplyPayload, info: { kind: ReplyLifecycleKind }) => {
const deliveryPayload = resolveWhatsAppDeliverablePayload(payload, info, {
exposeErrorText,
});
const deliveryPayload = resolveWhatsAppDeliverablePayload(payload, info);
if (!deliveryPayload) {
return;
}

View File

@@ -63,20 +63,6 @@ describe("whatsapp config schema", () => {
}
});
it("accepts exposeErrorText at channel and account scope", () => {
const res = expectWhatsAppConfigValid({
exposeErrorText: false,
accounts: {
work: { exposeErrorText: true },
},
});
if (res.success) {
expect(res.data.exposeErrorText).toBe(false);
expect(res.data.accounts?.work?.exposeErrorText).toBe(true);
}
});
it("accepts enabled", () => {
expectWhatsAppConfigValid({
enabled: true,

View File

@@ -21,8 +21,4 @@ export const whatsAppChannelConfigUiHints = {
label: "WhatsApp Config Writes",
help: "Allow WhatsApp to write config in response to channel events/commands (default: true).",
},
exposeErrorText: {
label: "WhatsApp Error Text",
help: "Deliver user-visible agent/provider error text into WhatsApp (default: true). Disable to keep failures quiet on WhatsApp.",
},
} satisfies Record<string, ChannelConfigUiHint>;

View File

@@ -167,11 +167,11 @@ describe("whatsappOutbound sendPayload", () => {
expect(sendWhatsApp).not.toHaveBeenCalled();
});
it("suppresses routed error payloads when error text is hidden", async () => {
it("suppresses routed error payloads", async () => {
const sendWhatsApp = vi.fn();
const result = await whatsappOutbound.sendPayload!({
cfg: { channels: { whatsapp: { exposeErrorText: false } } },
cfg: {},
to: "5511999999999@c.us",
text: "",
payload: { text: "provider exploded", isError: true },
@@ -182,35 +182,6 @@ describe("whatsappOutbound sendPayload", () => {
expect(sendWhatsApp).not.toHaveBeenCalled();
});
it("uses account-level error text visibility for routed payloads", async () => {
const sendWhatsApp = vi.fn(async () => ({ messageId: "wa-1", toJid: "jid" }));
await whatsappOutbound.sendPayload!({
cfg: {
channels: {
whatsapp: {
exposeErrorText: false,
accounts: {
work: { exposeErrorText: true },
},
},
},
},
accountId: "work",
to: "5511999999999@c.us",
text: "",
payload: { text: "provider exploded", isError: true },
deps: { sendWhatsApp },
});
expect(sendWhatsApp).toHaveBeenCalledTimes(1);
expect(sendWhatsApp).toHaveBeenCalledWith(
"5511999999999@c.us",
"provider exploded",
expect.any(Object),
);
});
it("sanitizes HTML-only text to whitespace-only payload", () => {
expect(
whatsappOutbound

View File

@@ -11,7 +11,6 @@ import {
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types";
import { resolveOutboundSendDep } from "openclaw/plugin-sdk/outbound-send-deps";
import { sendTextMediaPayload } from "openclaw/plugin-sdk/reply-payload";
import { resolveMergedWhatsAppAccountConfig } from "./account-config.js";
import {
normalizeWhatsAppOutboundPayload,
normalizeWhatsAppPayloadText,
@@ -220,11 +219,7 @@ export function createWhatsAppOutboundBase({
return {
...outbound,
sendPayload: async (ctx) => {
if (
ctx.payload.isError === true &&
resolveMergedWhatsAppAccountConfig({ cfg: ctx.cfg, accountId: ctx.accountId })
.exposeErrorText === false
) {
if (ctx.payload.isError === true) {
return { channel: "whatsapp", messageId: "" };
}
const payload = normalizeWhatsAppOutboundPayload(ctx.payload, { normalizeText });

View File

@@ -15962,9 +15962,6 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
},
],
},
exposeErrorText: {
type: "boolean",
},
heartbeat: {
type: "object",
properties: {
@@ -16253,9 +16250,6 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
},
],
},
exposeErrorText: {
type: "boolean",
},
heartbeat: {
type: "object",
properties: {
@@ -16344,10 +16338,6 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
label: "WhatsApp Config Writes",
help: "Allow WhatsApp to write config in response to channel events/commands (default: true).",
},
exposeErrorText: {
label: "WhatsApp Error Text",
help: "Deliver user-visible agent/provider error text into WhatsApp (default: true). Disable to keep failures quiet on WhatsApp.",
},
},
unsupportedSecretRefSurfacePatterns: [
"channels.whatsapp.accounts.*.creds.json",

View File

@@ -105,8 +105,6 @@ type WhatsAppSharedConfig = {
debounceMs?: number;
/** Reply threading mode for auto-replies (off|first|all|batched). */
replyToMode?: ReplyToMode;
/** Whether WhatsApp should deliver user-visible error text. Default: true. */
exposeErrorText?: boolean;
/** Heartbeat visibility settings. */
heartbeat?: ChannelHeartbeatVisibilityConfig;
/** Channel health monitor overrides for this channel/account. */

View File

@@ -72,21 +72,6 @@ describe("WhatsApp prompt config Zod validation", () => {
}
});
it("validates exposeErrorText at root and account scope", () => {
const result = WhatsAppConfigSchema.safeParse({
exposeErrorText: false,
accounts: {
work: { exposeErrorText: true },
},
});
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.exposeErrorText).toBe(false);
expect(result.data.accounts?.work?.exposeErrorText).toBe(true);
}
});
it("validates WhatsAppAccountSchema directly", () => {
const accountConfig = {
name: "Personal Account",

View File

@@ -83,7 +83,6 @@ function buildWhatsAppCommonShape(params: { useDefaults: boolean }) {
? z.number().int().nonnegative().optional().default(0)
: z.number().int().nonnegative().optional(),
replyToMode: ReplyToModeSchema.optional(),
exposeErrorText: z.boolean().optional(),
heartbeat: ChannelHeartbeatVisibilitySchema,
healthMonitor: ChannelHealthMonitorSchema,
};