fix(outbound): clamp message gateway timeouts

This commit is contained in:
Peter Steinberger
2026-05-30 16:50:04 -04:00
parent 2cbfb910f2
commit c8d458d13d
4 changed files with 63 additions and 50 deletions
+2 -18
View File
@@ -41,8 +41,6 @@ import { stripUnsupportedCitationControlMarkers } from "../../shared/text/citati
import { stripFormattedReasoningMessage } from "../../shared/text/formatted-reasoning-message.js";
import { parseInlineDirectives } from "../../utils/directive-tags.js";
import {
GATEWAY_CLIENT_MODES,
GATEWAY_CLIENT_NAMES,
INTERNAL_MESSAGE_CHANNEL,
type GatewayClientMode,
type GatewayClientName,
@@ -73,6 +71,7 @@ import {
resolveAndApplyOutboundThreadId,
} from "./message-action-threading.js";
import { maybeApplyTtsToMessageActionSendPayload } from "./message-action-tts.js";
import { resolveOutboundMessageGatewayOptions } from "./message-gateway-options.js";
import type { MessagePollResult, MessageSendResult } from "./message.js";
import {
applyCrossContextDecoration,
@@ -187,22 +186,7 @@ export function getToolResult(
}
function resolveGatewayActionOptions(gateway?: MessageActionRunnerGateway) {
const url =
gateway?.mode === GATEWAY_CLIENT_MODES.BACKEND ||
gateway?.clientName === GATEWAY_CLIENT_NAMES.GATEWAY_CLIENT
? undefined
: gateway?.url;
return {
url,
token: gateway?.token,
timeoutMs:
typeof gateway?.timeoutMs === "number" && Number.isFinite(gateway.timeoutMs)
? Math.max(1, Math.floor(gateway.timeoutMs))
: 10_000,
clientName: gateway?.clientName ?? GATEWAY_CLIENT_NAMES.CLI,
clientDisplayName: gateway?.clientDisplayName,
mode: gateway?.mode ?? GATEWAY_CLIENT_MODES.CLI,
};
return resolveOutboundMessageGatewayOptions(gateway);
}
async function callGatewayMessageAction<T>(params: {
@@ -0,0 +1,22 @@
import { describe, expect, it } from "vitest";
import { MAX_TIMER_TIMEOUT_MS } from "../../shared/number-coercion.js";
import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../../utils/message-channel.js";
import { resolveOutboundMessageGatewayOptions } from "./message-gateway-options.js";
describe("resolveOutboundMessageGatewayOptions", () => {
it("clamps oversized gateway timeouts", () => {
expect(
resolveOutboundMessageGatewayOptions({ timeoutMs: Number.MAX_SAFE_INTEGER }).timeoutMs,
).toBe(MAX_TIMER_TIMEOUT_MS);
});
it("drops caller-provided urls for backend gateway callers", () => {
expect(
resolveOutboundMessageGatewayOptions({
url: "http://attacker.invalid",
clientName: GATEWAY_CLIENT_NAMES.GATEWAY_CLIENT,
mode: GATEWAY_CLIENT_MODES.BACKEND,
}).url,
).toBeUndefined();
});
});
@@ -0,0 +1,33 @@
import { resolveTimerTimeoutMs } from "../../shared/number-coercion.js";
import {
GATEWAY_CLIENT_MODES,
GATEWAY_CLIENT_NAMES,
type GatewayClientMode,
type GatewayClientName,
} from "../../utils/message-channel.js";
export type OutboundMessageGatewayOptionsInput = {
url?: string;
token?: string;
timeoutMs?: number;
clientName?: GatewayClientName;
clientDisplayName?: string;
mode?: GatewayClientMode;
};
export function resolveOutboundMessageGatewayOptions(gateway?: OutboundMessageGatewayOptionsInput) {
const clientName = gateway?.clientName ?? GATEWAY_CLIENT_NAMES.CLI;
const mode = gateway?.mode ?? GATEWAY_CLIENT_MODES.CLI;
const url =
mode === GATEWAY_CLIENT_MODES.BACKEND || clientName === GATEWAY_CLIENT_NAMES.GATEWAY_CLIENT
? undefined
: gateway?.url;
return {
url,
token: gateway?.token,
timeoutMs: resolveTimerTimeoutMs(gateway?.timeoutMs, 10_000),
clientName,
clientDisplayName: gateway?.clientDisplayName,
mode,
};
}
+6 -32
View File
@@ -5,12 +5,6 @@ import type { OpenClawConfig } from "../../config/types.openclaw.js";
import type { OutboundMediaAccess } from "../../media/load-options.js";
import type { PollInput } from "../../polls.js";
import { normalizePollInput } from "../../polls.js";
import {
GATEWAY_CLIENT_MODES,
GATEWAY_CLIENT_NAMES,
type GatewayClientMode,
type GatewayClientName,
} from "../../utils/message-channel.js";
import { resolveOutboundChannelPlugin } from "./channel-resolution.js";
import { resolveMessageChannelSelection } from "./channel-selection.js";
import {
@@ -20,6 +14,10 @@ import {
type OutboundDeliveryQueuePolicy,
type OutboundSendDeps,
} from "./deliver.js";
import {
resolveOutboundMessageGatewayOptions,
type OutboundMessageGatewayOptionsInput,
} from "./message-gateway-options.js";
import type { OutboundMirror } from "./mirror.js";
import {
createOutboundPayloadPlan,
@@ -44,14 +42,7 @@ function loadMessageGatewayRuntime() {
return messageGatewayRuntimePromise;
}
export type MessageGatewayOptions = {
url?: string;
token?: string;
timeoutMs?: number;
clientName?: GatewayClientName;
clientDisplayName?: string;
mode?: GatewayClientMode;
};
export type MessageGatewayOptions = OutboundMessageGatewayOptionsInput;
type MessageSendParams = {
to: string;
@@ -261,24 +252,7 @@ async function assertRequiredMessageSendDurability(params: {
}
function resolveGatewayOptions(opts?: MessageGatewayOptions) {
// Security: backend callers (tools/agents) must not accept user-controlled gateway URLs.
// Use config-derived gateway target only.
const url =
opts?.mode === GATEWAY_CLIENT_MODES.BACKEND ||
opts?.clientName === GATEWAY_CLIENT_NAMES.GATEWAY_CLIENT
? undefined
: opts?.url;
return {
url,
token: opts?.token,
timeoutMs:
typeof opts?.timeoutMs === "number" && Number.isFinite(opts.timeoutMs)
? Math.max(1, Math.floor(opts.timeoutMs))
: 10_000,
clientName: opts?.clientName ?? GATEWAY_CLIENT_NAMES.CLI,
clientDisplayName: opts?.clientDisplayName,
mode: opts?.mode ?? GATEWAY_CLIENT_MODES.CLI,
};
return resolveOutboundMessageGatewayOptions(opts);
}
async function callMessageGateway<T>(params: {