From f2fa096f14f07dbb3bfd3f56bf4adf3ba50a5fff Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 7 Apr 2026 12:20:52 +0100 Subject: [PATCH] refactor: dedupe gateway lowercase helpers --- src/gateway/gateway-config-prompts.shared.ts | 5 +++-- src/gateway/live-tool-probe-utils.ts | 8 +++++--- src/gateway/server-cron.ts | 7 +++++-- src/gateway/server-methods/talk.ts | 7 +++++-- src/gateway/sessions-patch.ts | 9 +++++---- src/gateway/tools-invoke-http.ts | 8 +++++--- 6 files changed, 28 insertions(+), 16 deletions(-) diff --git a/src/gateway/gateway-config-prompts.shared.ts b/src/gateway/gateway-config-prompts.shared.ts index 069e5c3c140..63539593500 100644 --- a/src/gateway/gateway-config-prompts.shared.ts +++ b/src/gateway/gateway-config-prompts.shared.ts @@ -1,6 +1,7 @@ import type { OpenClawConfig } from "../config/config.js"; import { getTailnetHostname } from "../infra/tailscale.js"; import { isIpv6Address, parseCanonicalIpAddress } from "../shared/net/ip.js"; +import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js"; export const TAILSCALE_EXPOSURE_OPTIONS = [ { value: "off", label: "Off", hint: "No Tailscale exposure" }, @@ -56,8 +57,8 @@ export function buildTailnetHttpsOrigin(rawHost: string): string | null { export function appendAllowedOrigin(existing: string[] | undefined, origin: string): string[] { const current = existing ?? []; - const normalized = origin.toLowerCase(); - if (current.some((entry) => entry.toLowerCase() === normalized)) { + const normalized = normalizeLowercaseStringOrEmpty(origin); + if (current.some((entry) => normalizeLowercaseStringOrEmpty(entry) === normalized)) { return current; } return [...current, origin]; diff --git a/src/gateway/live-tool-probe-utils.ts b/src/gateway/live-tool-probe-utils.ts index 7dce8c5e38b..0a4e0f7a1fa 100644 --- a/src/gateway/live-tool-probe-utils.ts +++ b/src/gateway/live-tool-probe-utils.ts @@ -1,3 +1,5 @@ +import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js"; + export function hasExpectedToolNonce(text: string, nonceA: string, nonceB: string): boolean { return text.includes(nonceA) && text.includes(nonceB); } @@ -34,7 +36,7 @@ const PROBE_REFUSAL_MARKERS = [ ]; export function isLikelyToolNonceRefusal(text: string): boolean { - const lower = text.toLowerCase(); + const lower = normalizeLowercaseStringOrEmpty(text); if (PROBE_REFUSAL_MARKERS.some((marker) => lower.includes(marker))) { return true; } @@ -49,7 +51,7 @@ function hasMalformedToolOutput(text: string): boolean { if (!trimmed) { return true; } - const lower = trimmed.toLowerCase(); + const lower = normalizeLowercaseStringOrEmpty(trimmed); if (trimmed.includes("[object Object]")) { return true; } @@ -91,7 +93,7 @@ export function shouldRetryToolReadProbe(params: { if (params.provider === "anthropic" && isLikelyToolNonceRefusal(params.text)) { return true; } - const lower = params.text.trim().toLowerCase(); + const lower = normalizeLowercaseStringOrEmpty(params.text); if (params.provider === "mistral" && (lower.includes("noncea=") || lower.includes("nonceb="))) { return true; } diff --git a/src/gateway/server-cron.ts b/src/gateway/server-cron.ts index 0b4791e2bd9..7b99f60dad3 100644 --- a/src/gateway/server-cron.ts +++ b/src/gateway/server-cron.ts @@ -35,7 +35,10 @@ import { enqueueSystemEvent } from "../infra/system-events.js"; import { getChildLogger } from "../logging.js"; import { normalizeAgentId, toAgentStoreSessionKey } from "../routing/session-key.js"; import { defaultRuntime } from "../runtime.js"; -import { normalizeOptionalString } from "../shared/string-coerce.js"; +import { + normalizeOptionalLowercaseString, + normalizeOptionalString, +} from "../shared/string-coerce.js"; export type GatewayCronState = { cron: CronService; @@ -64,7 +67,7 @@ function resolveCronWebhookTarget(params: { legacyNotify?: boolean; legacyWebhook?: string; }): CronWebhookTarget | null { - const mode = normalizeOptionalString(params.delivery?.mode)?.toLowerCase(); + const mode = normalizeOptionalLowercaseString(params.delivery?.mode); if (mode === "webhook") { const url = normalizeHttpWebhookUrl(params.delivery?.to); return url ? { url, source: "delivery" } : null; diff --git a/src/gateway/server-methods/talk.ts b/src/gateway/server-methods/talk.ts index 7d07f63c3ce..1508c6f3607 100644 --- a/src/gateway/server-methods/talk.ts +++ b/src/gateway/server-methods/talk.ts @@ -7,7 +7,10 @@ import { } from "../../config/talk.js"; import type { TalkConfigResponse, TalkProviderConfig } from "../../config/types.gateway.js"; import type { OpenClawConfig, TtsConfig, TtsProviderConfigMap } from "../../config/types.js"; -import { normalizeOptionalString } from "../../shared/string-coerce.js"; +import { + normalizeLowercaseStringOrEmpty, + normalizeOptionalString, +} from "../../shared/string-coerce.js"; import { canonicalizeSpeechProviderId, getSpeechProvider } from "../../tts/provider-registry.js"; import { synthesizeSpeech, type TtsDirectiveOverrides } from "../../tts/tts.js"; import { ADMIN_SCOPE, TALK_SECRETS_SCOPE } from "../operator-scopes.js"; @@ -55,7 +58,7 @@ function asStringRecord(value: unknown): Record | undefined { } function normalizeAliasKey(value: string): string { - return value.trim().toLowerCase(); + return normalizeLowercaseStringOrEmpty(value); } function resolveTalkVoiceId( diff --git a/src/gateway/sessions-patch.ts b/src/gateway/sessions-patch.ts index 11a6a064c02..8f24a2bb0a4 100644 --- a/src/gateway/sessions-patch.ts +++ b/src/gateway/sessions-patch.ts @@ -30,6 +30,7 @@ import { applyVerboseOverride, parseVerboseOverride } from "../sessions/level-ov import { applyModelOverrideToSessionEntry } from "../sessions/model-overrides.js"; import { normalizeSendPolicy } from "../sessions/send-policy.js"; import { parseSessionLabel } from "../sessions/session-label.js"; +import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js"; import { ErrorCodes, type ErrorShape, @@ -42,7 +43,7 @@ function invalid(message: string): { ok: false; error: ErrorShape } { } function normalizeExecSecurity(raw: string): "deny" | "allowlist" | "full" | undefined { - const normalized = raw.trim().toLowerCase(); + const normalized = normalizeOptionalLowercaseString(raw); if (normalized === "deny" || normalized === "allowlist" || normalized === "full") { return normalized; } @@ -50,7 +51,7 @@ function normalizeExecSecurity(raw: string): "deny" | "allowlist" | "full" | und } function normalizeExecAsk(raw: string): "off" | "on-miss" | "always" | undefined { - const normalized = raw.trim().toLowerCase(); + const normalized = normalizeOptionalLowercaseString(raw); if (normalized === "off" || normalized === "on-miss" || normalized === "always") { return normalized; } @@ -62,7 +63,7 @@ function supportsSpawnLineage(storeKey: string): boolean { } function normalizeSubagentRole(raw: string): "orchestrator" | "leaf" | undefined { - const normalized = raw.trim().toLowerCase(); + const normalized = normalizeOptionalLowercaseString(raw); if (normalized === "orchestrator" || normalized === "leaf") { return normalized; } @@ -70,7 +71,7 @@ function normalizeSubagentRole(raw: string): "orchestrator" | "leaf" | undefined } function normalizeSubagentControlScope(raw: string): "children" | "none" | undefined { - const normalized = raw.trim().toLowerCase(); + const normalized = normalizeOptionalLowercaseString(raw); if (normalized === "children" || normalized === "none") { return normalized; } diff --git a/src/gateway/tools-invoke-http.ts b/src/gateway/tools-invoke-http.ts index b780bd1f01b..efd24b69eca 100644 --- a/src/gateway/tools-invoke-http.ts +++ b/src/gateway/tools-invoke-http.ts @@ -8,7 +8,10 @@ import { loadConfig } from "../config/config.js"; import { resolveMainSessionKey } from "../config/sessions.js"; import { logWarn } from "../logger.js"; import { isTestDefaultMemorySlotDisabled } from "../plugins/config-state.js"; -import { normalizeOptionalString } from "../shared/string-coerce.js"; +import { + normalizeOptionalLowercaseString, + normalizeOptionalString, +} from "../shared/string-coerce.js"; import { normalizeMessageChannel } from "../utils/message-channel.js"; import type { AuthRateLimiter } from "./auth-rate-limit.js"; import type { ResolvedGatewayAuth } from "./auth.js"; @@ -52,8 +55,7 @@ function resolveMemoryToolDisableReasons(cfg: ReturnType): st const reasons: string[] = []; const plugins = cfg.plugins; const slotRaw = plugins?.slots?.memory; - const slotDisabled = - slotRaw === null || (typeof slotRaw === "string" && slotRaw.trim().toLowerCase() === "none"); + const slotDisabled = slotRaw === null || normalizeOptionalLowercaseString(slotRaw) === "none"; const pluginsDisabled = plugins?.enabled === false; const defaultDisabled = isTestDefaultMemorySlotDisabled(cfg);