refactor: dedupe gateway infra lowercase helpers

This commit is contained in:
Peter Steinberger
2026-04-07 19:43:52 +01:00
parent ad6bfc44d5
commit e51a00ffc7
17 changed files with 76 additions and 48 deletions

View File

@@ -13,6 +13,7 @@ import {
requestDevicePairing,
} from "../infra/device-pairing.js";
import { isTruthyEnvValue } from "../infra/env.js";
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
import { getFreePortBlockWithPermissionFallback } from "../test-utils/ports.js";
import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../utils/message-channel.js";
import { GatewayClient } from "./client.js";
@@ -101,8 +102,8 @@ export function resolveCliModelSwitchProbeTarget(
providerId: string,
modelRef: string,
): string | undefined {
const normalizedProvider = providerId.trim().toLowerCase();
const normalizedModelRef = modelRef.trim().toLowerCase();
const normalizedProvider = normalizeLowercaseStringOrEmpty(providerId);
const normalizedModelRef = normalizeLowercaseStringOrEmpty(modelRef);
if (normalizedProvider !== "claude-cli") {
return undefined;
}
@@ -254,7 +255,7 @@ async function connectClientOnce(params: {
}
function isRetryableGatewayConnectError(error: Error): boolean {
const message = error.message.toLowerCase();
const message = normalizeLowercaseStringOrEmpty(error.message);
return (
message.includes("gateway closed during connect (1000)") ||
message.includes("gateway connect timeout") ||

View File

@@ -38,7 +38,7 @@ function normalizeTailnetHostForUrl(rawHost: string): string | null {
}
const parsed = parseCanonicalIpAddress(trimmed);
if (parsed && isIpv6Address(parsed)) {
return `[${parsed.toString().toLowerCase()}]`;
return `[${normalizeLowercaseStringOrEmpty(parsed.toString())}]`;
}
return trimmed;
}

View File

@@ -1,5 +1,6 @@
import { normalizeModelRef } from "../agents/model-selection.js";
import { normalizeProviderId } from "../agents/provider-id.js";
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
export type CachedModelPricing = {
input: number;
@@ -24,7 +25,9 @@ function modelPricingCacheKey(provider: string, model: string): string {
if (!providerId || !modelId) {
return "";
}
return modelId.toLowerCase().startsWith(`${providerId.toLowerCase()}/`)
return normalizeLowercaseStringOrEmpty(modelId).startsWith(
`${normalizeLowercaseStringOrEmpty(providerId)}/`,
)
? modelId
: `${providerId}/${modelId}`;
}

View File

@@ -781,7 +781,7 @@ export function createGatewayHttpServer(opts: {
});
// Don't interfere with WebSocket upgrades; ws handles the 'upgrade' event.
if (String(req.headers.upgrade ?? "").toLowerCase() === "websocket") {
if (normalizeLowercaseStringOrEmpty(req.headers.upgrade) === "websocket") {
return;
}

View File

@@ -28,7 +28,10 @@ import { classifySessionKeyShape, normalizeAgentId } from "../../routing/session
import { defaultRuntime } from "../../runtime.js";
import { normalizeInputProvenance, type InputProvenance } from "../../sessions/input-provenance.js";
import { resolveSendPolicy } from "../../sessions/send-policy.js";
import { normalizeOptionalString } from "../../shared/string-coerce.js";
import {
normalizeOptionalLowercaseString,
normalizeOptionalString,
} from "../../shared/string-coerce.js";
import { createRunningTaskRun } from "../../tasks/task-executor.js";
import {
normalizeDeliveryContext,
@@ -502,7 +505,8 @@ export const agentHandlers: GatewayRequestHandlers = {
);
return;
}
const resetReason = resetCommandMatch[1]?.toLowerCase() === "new" ? "new" : "reset";
const resetReason =
normalizeOptionalLowercaseString(resetCommandMatch[1]) === "new" ? "new" : "reset";
const resetResult = await runSessionResetFromAgent({
key: requestedSessionKey,
reason: resetReason,

View File

@@ -13,6 +13,7 @@ import {
resolveMemoryRemDreamingConfig,
} from "../../memory-host-sdk/dreaming.js";
import { getActiveMemorySearchManager } from "../../plugins/memory-runtime.js";
import { normalizeOptionalLowercaseString } from "../../shared/string-coerce.js";
import { formatError } from "../server-utils.js";
import { asRecord, normalizeTrimmedString } from "./record-shared.js";
import type { GatewayRequestHandlers } from "./types.js";
@@ -437,7 +438,7 @@ function isManagedDreamingJob(
return true;
}
const name = normalizeTrimmedString(job.name);
const payloadKind = normalizeTrimmedString(job.payload?.kind)?.toLowerCase();
const payloadKind = normalizeOptionalLowercaseString(job.payload?.kind);
const payloadText = normalizeTrimmedString(job.payload?.text);
return (
name === params.name && payloadKind === "systemevent" && payloadText === params.payloadText

View File

@@ -33,7 +33,11 @@ import {
resolveAgentIdFromSessionKey,
toAgentStoreSessionKey,
} from "../../routing/session-key.js";
import { normalizeOptionalString, readStringValue } from "../../shared/string-coerce.js";
import {
normalizeOptionalLowercaseString,
normalizeOptionalString,
readStringValue,
} from "../../shared/string-coerce.js";
import { GATEWAY_CLIENT_IDS } from "../protocol/client-info.js";
import {
ErrorCodes,
@@ -805,7 +809,7 @@ export const sessionsHandlers: GatewayRequestHandlers = {
}
canonicalParentSessionKey = parent.canonicalKey;
}
const loweredRequestedKey = requestedKey?.toLowerCase();
const loweredRequestedKey = normalizeOptionalLowercaseString(requestedKey);
const key = requestedKey
? loweredRequestedKey === "global" || loweredRequestedKey === "unknown"
? loweredRequestedKey

View File

@@ -9,6 +9,7 @@ import type { TalkConfigResponse, TalkProviderConfig } from "../../config/types.
import type { OpenClawConfig, TtsConfig, TtsProviderConfigMap } from "../../config/types.js";
import {
normalizeLowercaseStringOrEmpty,
normalizeOptionalLowercaseString,
normalizeOptionalString,
} from "../../shared/string-coerce.js";
import { canonicalizeSpeechProviderId, getSpeechProvider } from "../../tts/provider-registry.js";
@@ -203,8 +204,8 @@ function inferMimeType(
outputFormat: string | undefined,
fileExtension: string | undefined,
): string | undefined {
const normalizedOutput = normalizeOptionalString(outputFormat)?.toLowerCase();
const normalizedExtension = normalizeOptionalString(fileExtension)?.toLowerCase();
const normalizedOutput = normalizeOptionalLowercaseString(outputFormat);
const normalizedExtension = normalizeOptionalLowercaseString(fileExtension);
if (
normalizedOutput === "mp3" ||
normalizedOutput?.startsWith("mp3_") ||

View File

@@ -4,6 +4,7 @@ import { resolveCanvasHostUrl } from "../../infra/canvas-host-url.js";
import { removeRemoteNodeInfo } from "../../infra/skills-remote.js";
import { upsertPresence } from "../../infra/system-presence.js";
import type { createSubsystemLogger } from "../../logging/subsystem.js";
import { normalizeLowercaseStringOrEmpty } from "../../shared/string-coerce.js";
import { truncateUtf16Safe } from "../../utils.js";
import { isWebchatClient } from "../../utils/message-channel.js";
import type { AuthRateLimiter } from "../auth-rate-limit.js";
@@ -231,7 +232,8 @@ export function attachGatewayWsConnectionHandler(params: AttachGatewayWsConnecti
const isNoisySwiftPmHelperClose = (userAgent: string | undefined, remote: string | undefined) =>
Boolean(
userAgent?.toLowerCase().includes("swiftpm-testing-helper") && isLoopbackAddress(remote),
normalizeLowercaseStringOrEmpty(userAgent).includes("swiftpm-testing-helper") &&
isLoopbackAddress(remote),
);
socket.once("close", (code, reason) => {

View File

@@ -1,4 +1,5 @@
import { verifyDeviceSignature } from "../../../infra/device-identity.js";
import { normalizeLowercaseStringOrEmpty } from "../../../shared/string-coerce.js";
import type { AuthRateLimiter } from "../../auth-rate-limit.js";
import type { GatewayAuthResult } from "../../auth.js";
import { buildDeviceAuthPayload, buildDeviceAuthPayloadV3 } from "../../device-auth.js";
@@ -41,7 +42,7 @@ function resolveBrowserOriginRateLimitKey(requestOrigin?: string): string {
return BROWSER_ORIGIN_LOOPBACK_RATE_LIMIT_IP;
}
try {
return `${BROWSER_ORIGIN_RATE_LIMIT_KEY_PREFIX}${new URL(trimmedOrigin).origin.toLowerCase()}`;
return `${BROWSER_ORIGIN_RATE_LIMIT_KEY_PREFIX}${normalizeLowercaseStringOrEmpty(new URL(trimmedOrigin).origin)}`;
} catch {
return BROWSER_ORIGIN_LOOPBACK_RATE_LIMIT_IP;
}

View File

@@ -56,6 +56,7 @@ import {
} from "../shared/avatar-policy.js";
import {
normalizeLowercaseStringOrEmpty,
normalizeOptionalLowercaseString,
normalizeOptionalString,
} from "../shared/string-coerce.js";
import { normalizeSessionDeliveryFields } from "../utils/delivery-context.js";
@@ -464,10 +465,10 @@ export function findStoreKeysIgnoreCase(
store: Record<string, unknown>,
targetKey: string,
): string[] {
const lowered = targetKey.toLowerCase();
const lowered = normalizeLowercaseStringOrEmpty(targetKey);
const matches: string[] = [];
for (const key of Object.keys(store)) {
if (key.toLowerCase() === lowered) {
if (normalizeLowercaseStringOrEmpty(key) === lowered) {
matches.push(key);
}
}
@@ -612,7 +613,7 @@ function normalizeFallbackList(values: readonly string[]): string[] {
if (!trimmed) {
continue;
}
const key = trimmed.toLowerCase();
const key = normalizeLowercaseStringOrEmpty(trimmed);
if (seen.has(key)) {
continue;
}
@@ -701,7 +702,7 @@ export function listAgentsForGateway(cfg: OpenClawConfig): {
}
function canonicalizeSessionKeyForAgent(agentId: string, key: string): string {
const lowered = key.toLowerCase();
const lowered = normalizeLowercaseStringOrEmpty(key);
if (lowered === "global" || lowered === "unknown") {
return lowered;
}
@@ -723,7 +724,7 @@ export function resolveSessionStoreKey(params: {
if (!raw) {
return raw;
}
const rawLower = raw.toLowerCase();
const rawLower = normalizeLowercaseStringOrEmpty(raw);
if (rawLower === "global" || rawLower === "unknown") {
return rawLower;
}
@@ -731,7 +732,7 @@ export function resolveSessionStoreKey(params: {
const parsed = parseAgentSessionKey(raw);
if (parsed) {
const agentId = normalizeAgentId(parsed.agentId);
const lowered = raw.toLowerCase();
const lowered = normalizeLowercaseStringOrEmpty(raw);
const canonical = canonicalizeMainSessionAlias({
cfg: params.cfg,
agentId,
@@ -743,7 +744,7 @@ export function resolveSessionStoreKey(params: {
return lowered;
}
const lowered = raw.toLowerCase();
const lowered = normalizeLowercaseStringOrEmpty(raw);
const rawMainKey = normalizeMainKey(params.cfg.session?.mainKey);
if (lowered === "main" || lowered === rawMainKey) {
return resolveMainSessionKey(params.cfg);
@@ -772,13 +773,13 @@ export function canonicalizeSpawnedByForAgent(
if (!raw) {
return undefined;
}
const lower = raw.toLowerCase();
const lower = normalizeLowercaseStringOrEmpty(raw);
if (lower === "global" || lower === "unknown") {
return lower;
}
let result: string;
if (raw.toLowerCase().startsWith("agent:")) {
result = raw.toLowerCase();
if (lower.startsWith("agent:")) {
result = lower;
} else {
result = `agent:${normalizeAgentId(agentId)}:${lower}`;
}
@@ -1077,7 +1078,7 @@ export async function resolveGatewayModelSupportsImages(params: {
(entry) =>
entry.id === params.model && (!params.provider || entry.provider === params.provider),
);
const normalizedProvider = normalizeOptionalString(params.provider)?.toLowerCase();
const normalizedProvider = normalizeOptionalLowercaseString(params.provider);
const normalizedCandidates = [
normalizeLowercaseStringOrEmpty(params.model),
normalizeLowercaseStringOrEmpty(modelEntry?.name),

View File

@@ -450,7 +450,7 @@ export function normalizeClawHubSha256Hex(value: string): string | null {
if (!/^[A-Fa-f0-9]{64}$/.test(trimmed)) {
return null;
}
return trimmed.toLowerCase();
return normalizeLowercaseStringOrEmpty(trimmed);
}
export function parseClawHubPluginSpec(raw: string): {

View File

@@ -220,7 +220,7 @@ export function parseExecApprovalCommandText(
if (!match) {
return null;
}
const rawDecision = match[2].toLowerCase();
const rawDecision = normalizeOptionalLowercaseString(match[2]) ?? "";
return {
approvalId: match[1],
decision:

View File

@@ -1,6 +1,7 @@
import path from "node:path";
import {
normalizeLowercaseStringOrEmpty,
normalizeOptionalLowercaseString,
normalizeOptionalString,
} from "../shared/string-coerce.js";
import { isDispatchWrapperExecutable } from "./dispatch-wrapper-resolution.js";
@@ -81,7 +82,7 @@ export function isSafeBinUsage(params: {
return false;
}
const resolution = params.resolution;
const execName = resolution?.executableName?.toLowerCase();
const execName = normalizeOptionalLowercaseString(resolution?.executableName);
if (!execName) {
return false;
}
@@ -153,7 +154,7 @@ function pickExecAllowlistContext(params: ExecAllowlistContext): ExecAllowlistCo
}
function normalizeSkillBinName(value: string | undefined): string | null {
const trimmed = normalizeOptionalString(value)?.toLowerCase();
const trimmed = normalizeOptionalLowercaseString(value);
return trimmed && trimmed.length > 0 ? trimmed : null;
}
@@ -164,7 +165,7 @@ function normalizeSkillBinResolvedPath(value: string | undefined): string | null
}
const resolved = path.resolve(trimmed);
if (process.platform === "win32") {
return resolved.replace(/\\/g, "/").toLowerCase();
return normalizeLowercaseStringOrEmpty(resolved.replace(/\\/g, "/"));
}
return resolved;
}
@@ -224,7 +225,7 @@ function resolveSkillPreludePath(rawPath: string, cwd?: string): string {
function isSkillMarkdownPreludePath(filePath: string): boolean {
const normalized = filePath.replace(/\\/g, "/");
const lowerNormalized = normalized.toLowerCase();
const lowerNormalized = normalizeLowercaseStringOrEmpty(normalized);
if (!lowerNormalized.endsWith("/skill.md")) {
return false;
}
@@ -246,7 +247,7 @@ function isSkillMarkdownPreludePath(filePath: string): boolean {
function resolveSkillMarkdownPreludeId(filePath: string): string | null {
const normalized = filePath.replace(/\\/g, "/");
const lowerNormalized = normalized.toLowerCase();
const lowerNormalized = normalizeLowercaseStringOrEmpty(normalized);
if (!lowerNormalized.endsWith("/skill.md")) {
return null;
}
@@ -269,7 +270,7 @@ function resolveSkillMarkdownPreludeId(filePath: string): string | null {
function isSkillPreludeReadSegment(segment: ExecCommandSegment, cwd?: string): boolean {
const execution = resolveExecutionTargetResolution(segment.resolution);
if (execution?.executableName?.toLowerCase() !== "cat") {
if (normalizeLowercaseStringOrEmpty(execution?.executableName) !== "cat") {
return false;
}
// Keep the display-prelude exception narrow: only a plain `cat <...>/SKILL.md`
@@ -286,7 +287,7 @@ function isSkillPreludeReadSegment(segment: ExecCommandSegment, cwd?: string): b
function isSkillPreludeMarkerSegment(segment: ExecCommandSegment): boolean {
const execution = resolveExecutionTargetResolution(segment.resolution);
if (execution?.executableName?.toLowerCase() !== "printf") {
if (normalizeLowercaseStringOrEmpty(execution?.executableName) !== "printf") {
return false;
}
if (segment.argv.length !== 2) {
@@ -389,14 +390,18 @@ function resolveShellWrapperScriptArgv(params: {
effectiveArgv: string[];
cwd?: string;
}): string[] {
const scriptBase = path.basename(params.shellScriptCandidatePath).toLowerCase();
const scriptBase = normalizeLowercaseStringOrEmpty(
path.basename(params.shellScriptCandidatePath),
);
const cwdBase = params.cwd && params.cwd.trim() ? params.cwd.trim() : process.cwd();
const resolveArgPath = (a: string): string => (path.isAbsolute(a) ? a : path.resolve(cwdBase, a));
let idx = params.effectiveArgv.findIndex(
(a) => resolveArgPath(a) === params.shellScriptCandidatePath,
);
if (idx === -1) {
idx = params.effectiveArgv.findIndex((a) => path.basename(a).toLowerCase() === scriptBase);
idx = params.effectiveArgv.findIndex(
(a) => normalizeLowercaseStringOrEmpty(path.basename(a)) === scriptBase,
);
}
const scriptArgs = idx !== -1 ? params.effectiveArgv.slice(idx + 1) : [];
return [params.shellScriptCandidatePath, ...scriptArgs];
@@ -883,13 +888,15 @@ function buildScriptArgPatternFromArgv(
if (!isWindowsPlatform(platform ?? process.platform)) {
return undefined;
}
const scriptBase = path.basename(scriptPath).toLowerCase();
const scriptBase = normalizeLowercaseStringOrEmpty(path.basename(scriptPath));
const base = cwd && cwd.trim() ? cwd.trim() : process.cwd();
const resolveArgPath = (arg: string): string =>
path.isAbsolute(arg) ? arg : path.resolve(base, arg);
let scriptIdx = argv.findIndex((arg) => resolveArgPath(arg) === scriptPath);
if (scriptIdx === -1) {
scriptIdx = argv.findIndex((arg) => path.basename(arg).toLowerCase() === scriptBase);
scriptIdx = argv.findIndex(
(arg) => normalizeLowercaseStringOrEmpty(path.basename(arg)) === scriptBase,
);
}
const scriptArgs = scriptIdx !== -1 ? argv.slice(scriptIdx + 1) : [];
const normalized = scriptArgs.map((a) => a.replace(/\//g, "\\"));

View File

@@ -103,13 +103,16 @@ function hasSqliteSignal(err: unknown): boolean {
}
}
const name = readErrorName(err);
if (name.toLowerCase().includes("sqlite")) {
const name = normalizeLowercaseStringOrEmpty(readErrorName(err));
if (name.includes("sqlite")) {
return true;
}
const message = "message" in err && typeof err.message === "string" ? err.message : "";
if (message.toLowerCase().includes("sqlite")) {
const message =
"message" in err && typeof err.message === "string"
? normalizeLowercaseStringOrEmpty(err.message)
: "";
if (message.includes("sqlite")) {
return true;
}

View File

@@ -1,6 +1,7 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
import { CONFIG_DIR, ensureDir } from "../utils.js";
export function normalizeWideAreaDomain(raw?: string | null): string | null {
@@ -29,9 +30,7 @@ export function getWideAreaZonePath(domain: string): string {
}
function dnsLabel(raw: string, fallback: string): string {
const normalized = raw
.trim()
.toLowerCase()
const normalized = normalizeLowercaseStringOrEmpty(raw)
.replace(/[^a-z0-9-]+/g, "-")
.replace(/^-+/, "")
.replace(/-+$/, "");

View File

@@ -1,6 +1,7 @@
import { execFileSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
const DEFAULT_SYSTEM_ROOT = "C:\\Windows";
const DEFAULT_PROGRAM_FILES = "C:\\Program Files";
@@ -102,7 +103,7 @@ function getWindowsRegExeCandidates(env: Record<string, string | undefined>): re
if (!root) {
continue;
}
const key = root.toLowerCase();
const key = normalizeLowercaseStringOrEmpty(root);
if (seen.has(key)) {
continue;
}
@@ -239,7 +240,7 @@ export function getWindowsProgramFilesRoots(
if (!value) {
continue;
}
const key = value.toLowerCase();
const key = normalizeLowercaseStringOrEmpty(value);
if (seen.has(key)) {
continue;
}