refactor: centralize outbound target validation

This commit is contained in:
Peter Steinberger
2026-01-07 01:15:53 +00:00
parent 4fb9293c29
commit f171d509bb
5 changed files with 168 additions and 93 deletions

View File

@@ -1,6 +1,6 @@
# send-refactor scratchpad
- [ ] Commit + push current outbound refactor changes
- [x] Commit + push current outbound refactor changes
- [ ] Step 1: centralize outbound target validation
- [ ] Step 2: normalize payloads + single delivery call
- [ ] Step 3: unify outbound JSON/result formatting

View File

@@ -45,6 +45,7 @@ import {
registerAgentRunContext,
} from "../infra/agent-events.js";
import { deliverOutboundPayloads } from "../infra/outbound/deliver.js";
import { resolveOutboundTarget } from "../infra/outbound/targets.js";
import { defaultRuntime, type RuntimeEnv } from "../runtime.js";
import { resolveSendPolicy } from "../sessions/send-policy.js";
import { normalizeE164 } from "../utils.js";
@@ -501,93 +502,39 @@ export async function agentCommand(
const deliveryProvider =
deliveryProviderRaw === "imsg" ? "imessage" : deliveryProviderRaw;
const whatsappTarget = opts.to ? normalizeE164(opts.to) : allowFrom[0];
const telegramTarget = opts.to?.trim() || undefined;
const discordTarget = opts.to?.trim() || undefined;
const slackTarget = opts.to?.trim() || undefined;
const signalTarget = opts.to?.trim() || undefined;
const imessageTarget = opts.to?.trim() || undefined;
const logDeliveryError = (err: unknown) => {
const deliveryTarget =
deliveryProvider === "telegram"
? telegramTarget
: deliveryProvider === "whatsapp"
? whatsappTarget
: deliveryProvider === "discord"
? discordTarget
: deliveryProvider === "slack"
? slackTarget
: deliveryProvider === "signal"
? signalTarget
: deliveryProvider === "imessage"
? imessageTarget
: undefined;
const message = `Delivery failed (${deliveryProvider}${deliveryTarget ? ` to ${deliveryTarget}` : ""}): ${String(err)}`;
runtime.error?.(message);
if (!runtime.error) runtime.log(message);
};
const isDeliveryProviderKnown =
deliveryProvider === "whatsapp" ||
deliveryProvider === "telegram" ||
deliveryProvider === "discord" ||
deliveryProvider === "slack" ||
deliveryProvider === "signal" ||
deliveryProvider === "imessage" ||
deliveryProvider === "webchat";
const resolvedTarget =
deliver && isDeliveryProviderKnown
? resolveOutboundTarget({
provider: deliveryProvider,
to: opts.to,
allowFrom,
})
: null;
const deliveryTarget = resolvedTarget?.ok ? resolvedTarget.to : undefined;
if (deliver) {
if (deliveryProvider === "whatsapp" && !whatsappTarget) {
const err = new Error(
"Delivering to WhatsApp requires --to <E.164> or whatsapp.allowFrom[0]",
);
if (!bestEffortDeliver) throw err;
logDeliveryError(err);
}
if (deliveryProvider === "telegram" && !telegramTarget) {
const err = new Error("Delivering to Telegram requires --to <chatId>");
if (!bestEffortDeliver) throw err;
logDeliveryError(err);
}
if (deliveryProvider === "discord" && !discordTarget) {
const err = new Error(
"Delivering to Discord requires --to <channelId|user:ID|channel:ID>",
);
if (!bestEffortDeliver) throw err;
logDeliveryError(err);
}
if (deliveryProvider === "slack" && !slackTarget) {
const err = new Error(
"Delivering to Slack requires --to <channelId|user:ID|channel:ID>",
);
if (!bestEffortDeliver) throw err;
logDeliveryError(err);
}
if (deliveryProvider === "signal" && !signalTarget) {
const err = new Error(
"Delivering to Signal requires --to <E.164|group:ID|signal:group:ID|signal:+E.164>",
);
if (!bestEffortDeliver) throw err;
logDeliveryError(err);
}
if (deliveryProvider === "imessage" && !imessageTarget) {
const err = new Error(
"Delivering to iMessage requires --to <handle|chat_id:ID>",
);
if (!bestEffortDeliver) throw err;
logDeliveryError(err);
}
if (deliveryProvider === "webchat") {
const err = new Error(
"Delivering to WebChat is not supported via `clawdbot agent`; use WhatsApp/Telegram or run with --deliver=false.",
);
if (!bestEffortDeliver) throw err;
logDeliveryError(err);
}
if (
deliveryProvider !== "whatsapp" &&
deliveryProvider !== "telegram" &&
deliveryProvider !== "discord" &&
deliveryProvider !== "slack" &&
deliveryProvider !== "signal" &&
deliveryProvider !== "imessage" &&
deliveryProvider !== "webchat"
) {
if (!isDeliveryProviderKnown) {
const err = new Error(`Unknown provider: ${deliveryProvider}`);
if (!bestEffortDeliver) throw err;
logDeliveryError(err);
} else if (resolvedTarget && !resolvedTarget.ok) {
if (!bestEffortDeliver) throw resolvedTarget.error;
logDeliveryError(resolvedTarget.error);
}
}
@@ -639,24 +586,12 @@ export async function agentCommand(
deliveryProvider === "signal" ||
deliveryProvider === "imessage"
) {
const target =
deliveryProvider === "whatsapp"
? whatsappTarget
: deliveryProvider === "telegram"
? telegramTarget
: deliveryProvider === "discord"
? discordTarget
: deliveryProvider === "slack"
? slackTarget
: deliveryProvider === "signal"
? signalTarget
: imessageTarget;
if (!target) continue;
if (!deliveryTarget) continue;
try {
await deliverOutboundPayloads({
cfg,
provider: deliveryProvider,
to: target,
to: deliveryTarget,
payloads: [payload],
deps: {
sendWhatsApp: deps.sendMessageWhatsApp,

View File

@@ -4,6 +4,7 @@ import { callGateway, randomIdempotencyKey } from "../gateway/call.js";
import { success } from "../globals.js";
import { deliverOutboundPayloads } from "../infra/outbound/deliver.js";
import type { OutboundDeliveryResult } from "../infra/outbound/deliver.js";
import { resolveOutboundTarget } from "../infra/outbound/targets.js";
import type { RuntimeEnv } from "../runtime.js";
export async function sendCommand(
@@ -37,10 +38,17 @@ export async function sendCommand(
provider === "signal" ||
provider === "imessage"
) {
const resolvedTarget = resolveOutboundTarget({
provider,
to: opts.to,
});
if (!resolvedTarget.ok) {
throw resolvedTarget.error;
}
const results = await deliverOutboundPayloads({
cfg: loadConfig(),
provider,
to: opts.to,
to: resolvedTarget.to,
payloads: [{ text: opts.message, mediaUrl: opts.media }],
deps: {
sendWhatsApp: deps.sendMessageWhatsApp,

View File

@@ -0,0 +1,39 @@
import { describe, expect, it } from "vitest";
import { resolveOutboundTarget } from "./targets.js";
describe("resolveOutboundTarget", () => {
it("falls back to whatsapp allowFrom", () => {
const res = resolveOutboundTarget({
provider: "whatsapp",
to: "",
allowFrom: ["+1555"],
});
expect(res).toEqual({ ok: true, to: "+1555" });
});
it("normalizes whatsapp target when provided", () => {
const res = resolveOutboundTarget({
provider: "whatsapp",
to: " (555) 123-4567 ",
});
if (!res.ok) throw res.error;
expect(res.to).toBe("+5551234567");
});
it("rejects telegram with missing target", () => {
const res = resolveOutboundTarget({ provider: "telegram", to: " " });
expect(res.ok).toBe(false);
if (!res.ok) {
expect(res.error.message).toContain("Telegram");
}
});
it("rejects webchat delivery", () => {
const res = resolveOutboundTarget({ provider: "webchat", to: "x" });
expect(res.ok).toBe(false);
if (!res.ok) {
expect(res.error.message).toContain("WebChat");
}
});
});

View File

@@ -19,6 +19,99 @@ export type OutboundTarget = {
reason?: string;
};
export type OutboundTargetResolution =
| { ok: true; to: string }
| { ok: false; error: Error };
export function resolveOutboundTarget(params: {
provider:
| "whatsapp"
| "telegram"
| "discord"
| "slack"
| "signal"
| "imessage"
| "webchat";
to?: string;
allowFrom?: string[];
}): OutboundTargetResolution {
const trimmed = params.to?.trim() || "";
if (params.provider === "whatsapp") {
if (trimmed) {
return { ok: true, to: normalizeE164(trimmed) };
}
const fallback = params.allowFrom?.[0]?.trim();
if (fallback) {
return { ok: true, to: fallback };
}
return {
ok: false,
error: new Error(
"Delivering to WhatsApp requires --to <E.164> or whatsapp.allowFrom[0]",
),
};
}
if (params.provider === "telegram") {
if (!trimmed) {
return {
ok: false,
error: new Error("Delivering to Telegram requires --to <chatId>"),
};
}
return { ok: true, to: trimmed };
}
if (params.provider === "discord") {
if (!trimmed) {
return {
ok: false,
error: new Error(
"Delivering to Discord requires --to <channelId|user:ID|channel:ID>",
),
};
}
return { ok: true, to: trimmed };
}
if (params.provider === "slack") {
if (!trimmed) {
return {
ok: false,
error: new Error(
"Delivering to Slack requires --to <channelId|user:ID|channel:ID>",
),
};
}
return { ok: true, to: trimmed };
}
if (params.provider === "signal") {
if (!trimmed) {
return {
ok: false,
error: new Error(
"Delivering to Signal requires --to <E.164|group:ID|signal:group:ID|signal:+E.164>",
),
};
}
return { ok: true, to: trimmed };
}
if (params.provider === "imessage") {
if (!trimmed) {
return {
ok: false,
error: new Error(
"Delivering to iMessage requires --to <handle|chat_id:ID>",
),
};
}
return { ok: true, to: trimmed };
}
return {
ok: false,
error: new Error(
"Delivering to WebChat is not supported via `clawdbot agent`; use WhatsApp/Telegram or run with --deliver=false.",
),
};
}
export function resolveHeartbeatDeliveryTarget(params: {
cfg: ClawdbotConfig;
entry?: SessionEntry;