diff --git a/src/cli/capability-cli.ts b/src/cli/capability-cli.ts index 02343b92827..a8d3cefee8c 100644 --- a/src/cli/capability-cli.ts +++ b/src/cli/capability-cli.ts @@ -35,8 +35,11 @@ import { registerMemoryEmbeddingProvider, } from "../plugins/memory-embedding-providers.js"; import { writeRuntimeJson, defaultRuntime, type RuntimeEnv } from "../runtime.js"; -import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js"; -import { normalizeStringifiedOptionalString } from "../shared/string-coerce.js"; +import { + normalizeLowercaseStringOrEmpty, + normalizeOptionalString, + normalizeStringifiedOptionalString, +} from "../shared/string-coerce.js"; import { formatDocsLink } from "../terminal/links.js"; import { theme } from "../terminal/theme.js"; import { canonicalizeSpeechProviderId, listSpeechProviders } from "../tts/provider-registry.js"; @@ -878,7 +881,7 @@ async function runTtsConvert(params: { params: { text: params.text, channel: params.channel, - provider: params.provider?.trim() || undefined, + provider: normalizeOptionalString(params.provider), modelId: params.modelId, voiceId: params.voiceId, }, @@ -921,7 +924,9 @@ async function runTtsConvert(params: { voiceId: params.voiceId, }); const hasExplicitSelection = Boolean( - overrides.provider || params.modelId?.trim() || params.voiceId?.trim(), + overrides.provider || + normalizeOptionalString(params.modelId) || + normalizeOptionalString(params.voiceId), ); const result = await textToSpeech({ text: params.text, @@ -1004,7 +1009,7 @@ async function runTtsVoices(providerRaw?: string) { const cfg = loadConfig(); const config = resolveTtsConfig(cfg); const prefsPath = resolveTtsPrefsPath(config); - const provider = providerRaw?.trim() || getTtsProvider(config, prefsPath); + const provider = normalizeOptionalString(providerRaw) || getTtsProvider(config, prefsPath); return await listSpeechVoices({ provider, cfg, @@ -1106,7 +1111,7 @@ async function runMemoryEmbeddingCreate(params: { ensureMemoryEmbeddingProvidersRegistered(); const cfg = loadConfig(); const modelRef = resolveModelRefOverride(params.model); - const requestedProvider = params.provider?.trim() || modelRef.provider || "auto"; + const requestedProvider = normalizeOptionalString(params.provider) || modelRef.provider || "auto"; const result = await createEmbeddingProvider({ config: cfg, agentDir: resolveAgentDir(cfg, resolveDefaultAgentId(cfg)), diff --git a/src/cli/channel-auth.ts b/src/cli/channel-auth.ts index 83d8e6c04d9..f1d30f1f80e 100644 --- a/src/cli/channel-auth.ts +++ b/src/cli/channel-auth.ts @@ -15,6 +15,7 @@ import { applyPluginAutoEnable } from "../config/plugin-auto-enable.js"; import { setVerbose } from "../globals.js"; import { isBlockedObjectKey } from "../infra/prototype-keys.js"; import { defaultRuntime, type RuntimeEnv } from "../runtime.js"; +import { normalizeOptionalString } from "../shared/string-coerce.js"; import { sanitizeForLog } from "../terminal/ansi.js"; type ChannelAuthOptions = { @@ -128,7 +129,8 @@ function resolveAccountContext( opts: ChannelAuthOptions, cfg: OpenClawConfig, ) { - const accountId = opts.account?.trim() || resolveChannelDefaultAccountId({ plugin, cfg }); + const accountId = + normalizeOptionalString(opts.account) || resolveChannelDefaultAccountId({ plugin, cfg }); return { accountId }; } diff --git a/src/cli/completion-cli.ts b/src/cli/completion-cli.ts index 565597b9763..10773ddca43 100644 --- a/src/cli/completion-cli.ts +++ b/src/cli/completion-cli.ts @@ -4,7 +4,10 @@ import path from "node:path"; import { Command, Option } from "commander"; import { resolveStateDir } from "../config/paths.js"; import { routeLogsToStderr } from "../logging/console.js"; -import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js"; +import { + normalizeLowercaseStringOrEmpty, + normalizeOptionalString, +} from "../shared/string-coerce.js"; import { formatDocsLink } from "../terminal/links.js"; import { theme } from "../terminal/theme.js"; import { pathExists } from "../utils.js"; @@ -24,7 +27,7 @@ function isCompletionShell(value: string): value is CompletionShell { } export function resolveShellFromEnv(env: NodeJS.ProcessEnv = process.env): CompletionShell { - const shellPath = env.SHELL?.trim() ?? ""; + const shellPath = normalizeOptionalString(env.SHELL) ?? ""; const shellName = shellPath ? normalizeLowercaseStringOrEmpty(path.basename(shellPath)) : ""; if (shellName === "zsh") { return "zsh"; diff --git a/src/cli/container-target.ts b/src/cli/container-target.ts index 8fc6720e198..372b162e8a6 100644 --- a/src/cli/container-target.ts +++ b/src/cli/container-target.ts @@ -1,5 +1,6 @@ import { spawnSync } from "node:child_process"; import { consumeRootOptionToken, FLAG_TERMINATOR } from "../infra/cli-root-options.js"; +import { normalizeOptionalString } from "../shared/string-coerce.js"; import { resolveCliArgvInvocation } from "./argv-invocation.js"; import { forwardConsumedCliRootOption } from "./root-option-forward.js"; import { takeCliRootOptionValue } from "./root-option-value.js"; @@ -77,7 +78,7 @@ export function resolveCliContainerTarget( if (!parsed.ok) { throw new Error(parsed.error); } - return parsed.container ?? env.OPENCLAW_CONTAINER?.trim() ?? null; + return parsed.container ?? normalizeOptionalString(env.OPENCLAW_CONTAINER) ?? null; } function isContainerRunning(params: { diff --git a/src/cli/cron-cli/register.cron-edit.ts b/src/cli/cron-cli/register.cron-edit.ts index d6aad44bc25..b689b470360 100644 --- a/src/cli/cron-cli/register.cron-edit.ts +++ b/src/cli/cron-cli/register.cron-edit.ts @@ -285,7 +285,7 @@ export function registerCronEditCommand(cron: Command) { failureAlert.channel = normalizeOptionalLowercaseString(opts.failureAlertChannel); } if (hasFailureAlertTo) { - const to = String(opts.failureAlertTo).trim(); + const to = normalizeOptionalString(opts.failureAlertTo) ?? ""; failureAlert.to = to ? to : undefined; } if (hasFailureAlertCooldown) { @@ -303,7 +303,7 @@ export function registerCronEditCommand(cron: Command) { failureAlert.mode = mode; } if (hasFailureAlertAccountId) { - const accountId = String(opts.failureAlertAccountId).trim(); + const accountId = normalizeOptionalString(opts.failureAlertAccountId) ?? ""; failureAlert.accountId = accountId ? accountId : undefined; } patch.failureAlert = failureAlert; diff --git a/src/cli/directory-cli.ts b/src/cli/directory-cli.ts index 1c3e2512389..ad48d4d7008 100644 --- a/src/cli/directory-cli.ts +++ b/src/cli/directory-cli.ts @@ -144,7 +144,8 @@ export function registerDirectoryCli(program: Command) { if (!plugin) { throw new Error(`Unsupported channel: ${String(channelId)}`); } - const accountId = opts.account?.trim() || resolveChannelDefaultAccountId({ plugin, cfg }); + const accountId = + normalizeOptionalString(opts.account) || resolveChannelDefaultAccountId({ plugin, cfg }); return { cfg, channelId, accountId, plugin }; }; diff --git a/src/cli/exec-approvals-cli.ts b/src/cli/exec-approvals-cli.ts index d681e14150f..d4ad975fef1 100644 --- a/src/cli/exec-approvals-cli.ts +++ b/src/cli/exec-approvals-cli.ts @@ -15,6 +15,7 @@ import { } from "../infra/exec-approvals.js"; import { formatTimeAgo } from "../infra/format-time/format-relative.ts"; import { defaultRuntime } from "../runtime.js"; +import { normalizeOptionalString } from "../shared/string-coerce.js"; import { formatDocsLink } from "../terminal/links.js"; import { getTerminalTableWidth, renderTable } from "../terminal/table.js"; import { isRich, theme } from "../terminal/theme.js"; @@ -58,7 +59,7 @@ async function resolveTargetNodeId(opts: ExecApprovalsCliOpts): Promise>, ) { - const host = opts.host?.trim() || config?.gateway?.host || "127.0.0.1"; + const host = normalizeOptionalString(opts.host) || config?.gateway?.host || "127.0.0.1"; const portOverride = parsePort(opts.port); if (opts.port !== undefined && portOverride === null) { return { host, port: null }; @@ -127,7 +128,8 @@ export async function runNodeDaemonInstall(opts: NodeDaemonInstallOptions) { return; } - const tlsFingerprint = opts.tlsFingerprint?.trim() || config?.gateway?.tlsFingerprint; + const tlsFingerprint = + normalizeOptionalString(opts.tlsFingerprint) || config?.gateway?.tlsFingerprint; const tls = Boolean(opts.tls) || Boolean(tlsFingerprint) || Boolean(config?.gateway?.tls); const { programArguments, workingDirectory, environment, description } = await buildNodeInstallPlan({ diff --git a/src/cli/node-cli/register.ts b/src/cli/node-cli/register.ts index b3cb08c79be..200aa951d79 100644 --- a/src/cli/node-cli/register.ts +++ b/src/cli/node-cli/register.ts @@ -1,6 +1,7 @@ import type { Command } from "commander"; import { loadNodeHostConfig } from "../../node-host/config.js"; import { runNodeHost } from "../../node-host/runner.js"; +import { normalizeOptionalString } from "../../shared/string-coerce.js"; import { formatDocsLink } from "../../terminal/links.js"; import { theme } from "../../terminal/theme.js"; import { parsePort } from "../daemon-cli/shared.js"; @@ -48,7 +49,9 @@ export function registerNodeCli(program: Command) { .action(async (opts) => { const existing = await loadNodeHostConfig(); const host = - (opts.host as string | undefined)?.trim() || existing?.gateway?.host || "127.0.0.1"; + normalizeOptionalString(opts.host as string | undefined) || + existing?.gateway?.host || + "127.0.0.1"; const port = parsePortWithFallback(opts.port, existing?.gateway?.port ?? 18789); await runNodeHost({ gatewayHost: host, diff --git a/src/cli/profile.ts b/src/cli/profile.ts index 9881a25c16e..b443a3cc50e 100644 --- a/src/cli/profile.ts +++ b/src/cli/profile.ts @@ -2,7 +2,10 @@ import os from "node:os"; import path from "node:path"; import { FLAG_TERMINATOR } from "../infra/cli-root-options.js"; import { resolveRequiredHomeDir } from "../infra/home-dir.js"; -import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js"; +import { + normalizeLowercaseStringOrEmpty, + normalizeOptionalString, +} from "../shared/string-coerce.js"; import { resolveCliArgvInvocation } from "./argv-invocation.js"; import { isValidProfileName } from "./profile-utils.js"; import { forwardConsumedCliRootOption } from "./root-option-forward.js"; @@ -103,12 +106,13 @@ export function applyCliProfileEnv(params: { // Convenience only: fill defaults, never override explicit env values. env.OPENCLAW_PROFILE = profile; - const stateDir = env.OPENCLAW_STATE_DIR?.trim() || resolveProfileStateDir(profile, env, homedir); - if (!env.OPENCLAW_STATE_DIR?.trim()) { + const existingStateDir = normalizeOptionalString(env.OPENCLAW_STATE_DIR); + const stateDir = existingStateDir || resolveProfileStateDir(profile, env, homedir); + if (!existingStateDir) { env.OPENCLAW_STATE_DIR = stateDir; } - if (!env.OPENCLAW_CONFIG_PATH?.trim()) { + if (!normalizeOptionalString(env.OPENCLAW_CONFIG_PATH)) { env.OPENCLAW_CONFIG_PATH = path.join(stateDir, "openclaw.json"); } diff --git a/src/cli/run-main.ts b/src/cli/run-main.ts index c327461d6b4..45d2e069ff3 100644 --- a/src/cli/run-main.ts +++ b/src/cli/run-main.ts @@ -15,6 +15,7 @@ import { hasMemoryRuntime } from "../plugins/memory-state.js"; import { normalizeLowercaseStringOrEmpty, normalizeOptionalLowercaseString, + normalizeOptionalString, } from "../shared/string-coerce.js"; import { resolveCliArgvInvocation } from "./argv-invocation.js"; import { @@ -115,7 +116,7 @@ export async function runCli(argv: string[] = process.argv) { applyCliProfileEnv({ profile: parsedProfile.profile }); } const containerTargetName = - parsedContainer.container ?? process.env.OPENCLAW_CONTAINER?.trim() ?? null; + parsedContainer.container ?? normalizeOptionalString(process.env.OPENCLAW_CONTAINER) ?? null; if (containerTargetName && parsedProfile.profile) { throw new Error("--container cannot be combined with --profile/--dev"); } diff --git a/src/cli/security-cli.ts b/src/cli/security-cli.ts index a6180c7c1cc..18a541c0ddc 100644 --- a/src/cli/security-cli.ts +++ b/src/cli/security-cli.ts @@ -3,6 +3,7 @@ import { loadConfig } from "../config/config.js"; import { defaultRuntime } from "../runtime.js"; import { runSecurityAudit } from "../security/audit.js"; import { fixSecurityFootguns } from "../security/fix.js"; +import { normalizeOptionalString } from "../shared/string-coerce.js"; import { formatDocsLink } from "../terminal/links.js"; import { isRich, theme } from "../terminal/theme.js"; import { shortenHomeInString, shortenHomePath } from "../utils.js"; @@ -60,6 +61,8 @@ export function registerSecurityCli(program: Command) { .option("--fix", "Apply safe fixes (tighten defaults + chmod state/config)", false) .option("--json", "Print JSON", false) .action(async (opts: SecurityAuditOptions) => { + const token = normalizeOptionalString(opts.token); + const password = normalizeOptionalString(opts.password); const fixResult = opts.fix ? await fixSecurityFootguns().catch((_err) => null) : null; const sourceConfig = loadConfig(); @@ -77,11 +80,8 @@ export function registerSecurityCli(program: Command) { includeFilesystem: true, includeChannelSecurity: true, deepProbeAuth: - opts.token?.trim() || opts.password?.trim() - ? { - ...(opts.token?.trim() ? { token: opts.token } : {}), - ...(opts.password?.trim() ? { password: opts.password } : {}), - } + token || password + ? { ...(token ? { token } : {}), ...(password ? { password } : {}) } : undefined, }); diff --git a/src/cli/update-cli/restart-helper.ts b/src/cli/update-cli/restart-helper.ts index a68fab161fa..f4bcb793ed3 100644 --- a/src/cli/update-cli/restart-helper.ts +++ b/src/cli/update-cli/restart-helper.ts @@ -9,6 +9,7 @@ import { resolveGatewaySystemdServiceName, resolveGatewayWindowsTaskName, } from "../../daemon/constants.js"; +import { normalizeOptionalString } from "../../shared/string-coerce.js"; /** * Shell-escape a string for embedding in single-quoted shell arguments. @@ -26,7 +27,7 @@ function isBatchSafe(value: string): boolean { } function resolveSystemdUnit(env: NodeJS.ProcessEnv): string { - const override = env.OPENCLAW_SYSTEMD_UNIT?.trim(); + const override = normalizeOptionalString(env.OPENCLAW_SYSTEMD_UNIT); if (override) { return override.endsWith(".service") ? override : `${override}.service`; } @@ -34,7 +35,7 @@ function resolveSystemdUnit(env: NodeJS.ProcessEnv): string { } function resolveLaunchdLabel(env: NodeJS.ProcessEnv): string { - const override = env.OPENCLAW_LAUNCHD_LABEL?.trim(); + const override = normalizeOptionalString(env.OPENCLAW_LAUNCHD_LABEL); if (override) { return override; } @@ -86,7 +87,7 @@ rm -f "$0" const uid = process.getuid ? process.getuid() : 501; // Resolve HOME at generation time via env/process.env to match launchd.ts, // and shell-escape the label in the plist filename to prevent injection. - const home = env.HOME?.trim() || process.env.HOME || os.homedir(); + const home = normalizeOptionalString(env.HOME) || process.env.HOME || os.homedir(); const plistPath = path.join(home, "Library", "LaunchAgents", `${label}.plist`); const escapedPlistPath = shellEscape(plistPath); filename = `openclaw-restart-${timestamp}.sh`; diff --git a/src/commands/agent-via-gateway.ts b/src/commands/agent-via-gateway.ts index f15874f70ce..c61c3b20439 100644 --- a/src/commands/agent-via-gateway.ts +++ b/src/commands/agent-via-gateway.ts @@ -7,6 +7,7 @@ import { loadConfig } from "../config/config.js"; import { callGateway, randomIdempotencyKey } from "../gateway/call.js"; import { normalizeAgentId } from "../routing/session-key.js"; import { type RuntimeEnv, writeRuntimeJson } from "../runtime.js"; +import { normalizeOptionalString } from "../shared/string-coerce.js"; import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES, @@ -119,7 +120,7 @@ export async function agentViaGatewayCommand(opts: AgentCliOpts, runtime: Runtim }).sessionKey; const channel = normalizeMessageChannel(opts.channel); - const idempotencyKey = opts.runId?.trim() || randomIdempotencyKey(); + const idempotencyKey = normalizeOptionalString(opts.runId) || randomIdempotencyKey(); const response = await withProgress( { diff --git a/src/commands/agents.bindings.ts b/src/commands/agents.bindings.ts index 009a1fddac8..1240e8d592e 100644 --- a/src/commands/agents.bindings.ts +++ b/src/commands/agents.bindings.ts @@ -5,24 +5,19 @@ import { isRouteBinding, listRouteBindings } from "../config/bindings.js"; import type { OpenClawConfig } from "../config/config.js"; import type { AgentRouteBinding } from "../config/types.js"; import { DEFAULT_ACCOUNT_ID, normalizeAgentId } from "../routing/session-key.js"; +import { normalizeOptionalString } from "../shared/string-coerce.js"; +import { normalizeStringEntries } from "../shared/string-normalization.js"; import type { ChannelChoice } from "./onboard-types.js"; function bindingMatchKey(match: AgentRouteBinding["match"]) { - const accountId = match.accountId?.trim() || DEFAULT_ACCOUNT_ID; + const accountId = normalizeOptionalString(match.accountId) || DEFAULT_ACCOUNT_ID; const identityKey = bindingMatchIdentityKey(match); return [identityKey, accountId].join("|"); } function bindingMatchIdentityKey(match: AgentRouteBinding["match"]) { const roles = Array.isArray(match.roles) - ? Array.from( - new Set( - match.roles - .map((role) => role.trim()) - .filter(Boolean) - .toSorted(), - ), - ) + ? Array.from(new Set(normalizeStringEntries(match.roles).toSorted())) : []; return [ match.channel, @@ -39,10 +34,10 @@ function canUpgradeBindingAccountScope(params: { incoming: AgentRouteBinding; normalizedIncomingAgentId: string; }): boolean { - if (!params.incoming.match.accountId?.trim()) { + if (!normalizeOptionalString(params.incoming.match.accountId)) { return false; } - if (params.existing.match.accountId?.trim()) { + if (normalizeOptionalString(params.existing.match.accountId)) { return false; } if (normalizeAgentId(params.existing.agentId) !== params.normalizedIncomingAgentId) { diff --git a/src/commands/configure.gateway.ts b/src/commands/configure.gateway.ts index fd6f6b45b58..7142b2e202c 100644 --- a/src/commands/configure.gateway.ts +++ b/src/commands/configure.gateway.ts @@ -11,7 +11,8 @@ import { findTailscaleBinary } from "../infra/tailscale.js"; import type { RuntimeEnv } from "../runtime.js"; import { resolveDefaultSecretProviderAlias } from "../secrets/ref-contract.js"; import { validateIPv4AddressInput } from "../shared/net/ipv4.js"; -import { readStringValue } from "../shared/string-coerce.js"; +import { normalizeOptionalString, readStringValue } from "../shared/string-coerce.js"; +import { normalizeStringEntries } from "../shared/string-normalization.js"; import { note } from "../terminal/note.js"; import { buildGatewayAuthConfig } from "./configure.gateway-auth.js"; import { confirm, select, text } from "./configure.shared.js"; @@ -196,7 +197,7 @@ export async function promptGatewayConfig( initialValue: "OPENCLAW_GATEWAY_TOKEN", placeholder: "OPENCLAW_GATEWAY_TOKEN", validate: (value) => { - const candidate = String(value ?? "").trim(); + const candidate = normalizeOptionalString(value) ?? ""; if (!isValidEnvSecretRefId(candidate)) { return "Use an env var name like OPENCLAW_GATEWAY_TOKEN."; } @@ -209,7 +210,7 @@ export async function promptGatewayConfig( }), runtime, ); - const envVarName = String(envVar ?? "").trim(); + const envVarName = normalizeOptionalString(envVar) ?? ""; gatewayToken = { source: "env", provider: resolveDefaultSecretProviderAlias(cfg, "env", { @@ -239,7 +240,7 @@ export async function promptGatewayConfig( }), runtime, ); - gatewayPassword = String(password ?? "").trim(); + gatewayPassword = normalizeOptionalString(password) ?? ""; } if (authMode === "trusted-proxy") { @@ -273,10 +274,7 @@ export async function promptGatewayConfig( runtime, ); const requiredHeaders = requiredHeadersRaw - ? String(requiredHeadersRaw) - .split(",") - .map((h) => h.trim()) - .filter(Boolean) + ? normalizeStringEntries(String(requiredHeadersRaw).split(",")) : []; const allowUsersRaw = guardCancel( @@ -287,10 +285,7 @@ export async function promptGatewayConfig( runtime, ); const allowUsers = allowUsersRaw - ? String(allowUsersRaw) - .split(",") - .map((u) => u.trim()) - .filter(Boolean) + ? normalizeStringEntries(String(allowUsersRaw).split(",")) : []; const trustedProxiesRaw = guardCancel( @@ -298,7 +293,7 @@ export async function promptGatewayConfig( message: "Trusted proxy IPs (comma-separated)", placeholder: "10.0.1.10,192.168.1.5", validate: (value) => { - if (!value || String(value).trim() === "") { + if (!normalizeOptionalString(value)) { return "At least one trusted proxy IP is required"; } return undefined; @@ -306,13 +301,10 @@ export async function promptGatewayConfig( }), runtime, ); - trustedProxies = String(trustedProxiesRaw) - .split(",") - .map((ip) => ip.trim()) - .filter(Boolean); + trustedProxies = normalizeStringEntries(String(trustedProxiesRaw).split(",")); trustedProxyConfig = { - userHeader: String(userHeader).trim(), + userHeader: normalizeOptionalString(userHeader) ?? "", requiredHeaders: requiredHeaders.length > 0 ? requiredHeaders : undefined, allowUsers: allowUsers.length > 0 ? allowUsers : undefined, }; diff --git a/src/commands/doctor-memory-search.ts b/src/commands/doctor-memory-search.ts index a30fa0a130d..766f2587057 100644 --- a/src/commands/doctor-memory-search.ts +++ b/src/commands/doctor-memory-search.ts @@ -23,6 +23,7 @@ import { getActiveMemorySearchManager, resolveActiveMemoryBackendConfig, } from "../plugins/memory-runtime.js"; +import { normalizeOptionalString } from "../shared/string-coerce.js"; import { note } from "../terminal/note.js"; import { resolveUserPath } from "../utils.js"; import type { DoctorPrompter } from "./doctor-prompter.js"; @@ -358,7 +359,8 @@ export async function noteMemorySearchHealth( */ function hasLocalEmbeddings(local: { modelPath?: string }, useDefaultFallback = false): boolean { const modelPath = - local.modelPath?.trim() || (useDefaultFallback ? DEFAULT_LOCAL_MODEL : undefined); + normalizeOptionalString(local.modelPath) || + (useDefaultFallback ? DEFAULT_LOCAL_MODEL : undefined); if (!modelPath) { return false; } diff --git a/src/commands/doctor-plugin-manifests.ts b/src/commands/doctor-plugin-manifests.ts index 218f98283e6..bacb61f843a 100644 --- a/src/commands/doctor-plugin-manifests.ts +++ b/src/commands/doctor-plugin-manifests.ts @@ -2,6 +2,7 @@ import fs from "node:fs"; import { z } from "zod"; import { loadPluginManifestRegistry } from "../plugins/manifest-registry.js"; import type { RuntimeEnv } from "../runtime.js"; +import { normalizeOptionalString } from "../shared/string-coerce.js"; import { normalizeTrimmedStringList } from "../shared/string-normalization.js"; import { note } from "../terminal/note.js"; import { shortenHomePath } from "../utils.js"; @@ -69,7 +70,7 @@ function buildLegacyManifestContractMigration(params: { delete nextRaw.contracts; } - const pluginId = typeof params.raw.id === "string" ? params.raw.id.trim() : params.manifestPath; + const pluginId = normalizeOptionalString(params.raw.id) ?? params.manifestPath; return { manifestPath: params.manifestPath, pluginId, diff --git a/src/commands/gateway-status/discovery.ts b/src/commands/gateway-status/discovery.ts index c36d567e5f6..0095f9f1375 100644 --- a/src/commands/gateway-status/discovery.ts +++ b/src/commands/gateway-status/discovery.ts @@ -76,7 +76,7 @@ export async function resolveSshTarget(params: { } const identityFile = params.identity ?? - config.identityFiles.find((entry) => entry.trim().length > 0)?.trim() ?? + config.identityFiles.find((entry) => normalizeOptionalString(entry)) ?? undefined; return { target, identity: identityFile }; } diff --git a/src/commands/gateway-status/probe-run.ts b/src/commands/gateway-status/probe-run.ts index c76e7f83cc6..ba55a0380df 100644 --- a/src/commands/gateway-status/probe-run.ts +++ b/src/commands/gateway-status/probe-run.ts @@ -5,7 +5,7 @@ import { type GatewayBonjourBeacon, } from "../../infra/bonjour-discovery.js"; import { formatErrorMessage } from "../../infra/errors.js"; -import { readStringValue } from "../../shared/string-coerce.js"; +import { normalizeOptionalString, readStringValue } from "../../shared/string-coerce.js"; import { pickAutoSshTargetFromDiscovery } from "./discovery.js"; import { extractConfigSummary, @@ -86,7 +86,7 @@ export async function runGatewayStatusProbePass(params: { sshTarget = pickAutoSshTargetFromDiscovery({ discovery, parseSshTarget, - sshUser: process.env.USER?.trim() || "", + sshUser: normalizeOptionalString(process.env.USER) ?? "", }); } diff --git a/src/commands/message-format.ts b/src/commands/message-format.ts index 8f4fe9bd08c..cab2553c467 100644 --- a/src/commands/message-format.ts +++ b/src/commands/message-format.ts @@ -4,6 +4,8 @@ import type { OutboundDeliveryResult } from "../infra/outbound/deliver.js"; import { formatGatewaySummary, formatOutboundDeliverySummary } from "../infra/outbound/format.js"; import type { MessageActionRunResult } from "../infra/outbound/message-action-runner.js"; import { formatTargetDisplay } from "../infra/outbound/target-resolver.js"; +import { normalizeOptionalString } from "../shared/string-coerce.js"; +import { normalizeStringEntries } from "../shared/string-normalization.js"; import { getTerminalTableWidth, renderTable } from "../terminal/table.js"; import { isRich, theme } from "../terminal/theme.js"; import { shortenText } from "./text-format.js"; @@ -16,14 +18,16 @@ function extractMessageId(payload: unknown): string | null { return null; } const direct = (payload as { messageId?: unknown }).messageId; - if (typeof direct === "string" && direct.trim()) { - return direct.trim(); + const directId = normalizeOptionalString(direct); + if (directId) { + return directId; } const result = (payload as { result?: unknown }).result; if (result && typeof result === "object") { const nested = (result as { messageId?: unknown }).messageId; - if (typeof nested === "string" && nested.trim()) { - return nested.trim(); + const nestedId = normalizeOptionalString(nested); + if (nestedId) { + return nestedId; } } return null; @@ -356,10 +360,7 @@ export function formatMessageCliText(result: MessageActionRunResult): string[] { return lines; } if (Array.isArray(removed)) { - const list = removed - .map((x) => String(x).trim()) - .filter(Boolean) - .join(", "); + const list = normalizeStringEntries(removed).join(", "); lines.push(ok(`✅ Reactions removed${list ? `: ${list}` : ""}`)); return lines; } diff --git a/src/commands/models/auth.ts b/src/commands/models/auth.ts index 470fe66cf6e..b35bb607edb 100644 --- a/src/commands/models/auth.ts +++ b/src/commands/models/auth.ts @@ -31,6 +31,10 @@ import type { ProviderPlugin, } from "../../plugins/types.js"; import type { RuntimeEnv } from "../../runtime.js"; +import { + normalizeOptionalString, + normalizeStringifiedOptionalString, +} from "../../shared/string-coerce.js"; import { stylePromptHint, stylePromptMessage } from "../../terminal/prompt-style.js"; import { createClackPrompter } from "../../wizard/clack-prompter.js"; import { validateAnthropicSetupToken } from "../auth-token.js"; @@ -372,12 +376,13 @@ export async function modelsAuthPasteTokenCommand( runtime: RuntimeEnv, ) { const { agentDir } = await resolveModelsAuthContext(); - const rawProvider = opts.provider?.trim(); + const rawProvider = normalizeOptionalString(opts.provider); if (!rawProvider) { throw new Error("Missing --provider."); } const provider = normalizeProviderId(rawProvider); - const profileId = opts.profileId?.trim() || resolveDefaultTokenProfileId(provider); + const profileId = + normalizeOptionalString(opts.profileId) || resolveDefaultTokenProfileId(provider); const tokenInput = await text({ message: `Paste token for ${provider}`, @@ -397,12 +402,14 @@ export async function modelsAuthPasteTokenCommand( ? String(tokenInput ?? "") .replaceAll(/\s+/g, "") .trim() - : String(tokenInput ?? "").trim(); + : (normalizeOptionalString(tokenInput) ?? ""); - const expires = - opts.expiresIn?.trim() && opts.expiresIn.trim().length > 0 - ? Date.now() + parseDurationMs(String(opts.expiresIn ?? "").trim(), { defaultUnit: "d" }) - : undefined; + const expires = normalizeStringifiedOptionalString(opts.expiresIn) + ? Date.now() + + parseDurationMs(normalizeStringifiedOptionalString(opts.expiresIn) ?? "", { + defaultUnit: "d", + }) + : undefined; upsertAuthProfile({ profileId, diff --git a/src/commands/models/list.auth-overview.ts b/src/commands/models/list.auth-overview.ts index 122a46bf3da..e935eb6c92e 100644 --- a/src/commands/models/list.auth-overview.ts +++ b/src/commands/models/list.auth-overview.ts @@ -13,7 +13,10 @@ import { resolveUsableCustomProviderApiKey, } from "../../agents/model-auth.js"; import type { OpenClawConfig } from "../../config/config.js"; -import { normalizeLowercaseStringOrEmpty } from "../../shared/string-coerce.js"; +import { + normalizeLowercaseStringOrEmpty, + normalizeOptionalString, +} from "../../shared/string-coerce.js"; import { shortenHomePath } from "../../utils.js"; import { maskApiKey } from "./list.format.js"; import type { ProviderAuthOverview } from "./list.types.js"; @@ -29,7 +32,7 @@ function formatProfileSecretLabel(params: { ref: { source: string; id: string } | undefined; kind: "api-key" | "token"; }): string { - const value = typeof params.value === "string" ? params.value.trim() : ""; + const value = normalizeOptionalString(params.value) ?? ""; if (value) { const display = formatMarkerOrSecret(value); return params.kind === "token" ? `token:${display}` : display; diff --git a/src/commands/models/list.status-command.ts b/src/commands/models/list.status-command.ts index 0b4a2b59b48..802d6481022 100644 --- a/src/commands/models/list.status-command.ts +++ b/src/commands/models/list.status-command.ts @@ -40,6 +40,7 @@ import { } from "../../infra/provider-usage.js"; import { getShellEnvAppliedKeys, shouldEnableShellEnvFallback } from "../../infra/shell-env.js"; import { type RuntimeEnv, writeRuntimeJson } from "../../runtime.js"; +import { normalizeOptionalString } from "../../shared/string-coerce.js"; import { getTerminalTableWidth, renderTable } from "../../terminal/table.js"; import { colorize, theme } from "../../terminal/theme.js"; import { shortenHomePath } from "../../utils.js"; @@ -106,7 +107,7 @@ export async function modelsStatusCommand( const imageFallbacks = resolveAgentModelFallbackValues(cfg.agents?.defaults?.imageModel); const aliases = Object.entries(cfg.agents?.defaults?.models ?? {}).reduce>( (acc, [key, entry]) => { - const alias = typeof entry?.alias === "string" ? entry.alias.trim() : undefined; + const alias = normalizeOptionalString(entry?.alias); if (alias) { acc[alias] = key; } @@ -161,7 +162,7 @@ export async function modelsStatusCommand( ...providersFromEnv, ]), ) - .map((p) => (typeof p === "string" ? p.trim() : "")) + .map((p) => normalizeOptionalString(p) ?? "") .filter(Boolean) .toSorted((a, b) => a.localeCompare(b)); diff --git a/src/commands/onboard-non-interactive/local/auth-choice-inference.ts b/src/commands/onboard-non-interactive/local/auth-choice-inference.ts index a74b53dc369..3b088d44186 100644 --- a/src/commands/onboard-non-interactive/local/auth-choice-inference.ts +++ b/src/commands/onboard-non-interactive/local/auth-choice-inference.ts @@ -1,5 +1,6 @@ import type { OpenClawConfig } from "../../../config/config.js"; import { resolveManifestProviderOnboardAuthFlags } from "../../../plugins/provider-auth-choices.js"; +import { normalizeOptionalString } from "../../../shared/string-coerce.js"; import { CORE_ONBOARD_AUTH_FLAGS } from "../../onboard-core-auth-flags.js"; import type { AuthChoice, OnboardOptions } from "../../onboard-types.js"; @@ -15,7 +16,7 @@ export type AuthChoiceInference = { }; function hasStringValue(value: unknown): boolean { - return typeof value === "string" ? value.trim().length > 0 : Boolean(value); + return typeof value === "string" ? Boolean(normalizeOptionalString(value)) : Boolean(value); } // Infer auth choice from explicit provider API key flags. diff --git a/src/commands/sandbox-explain.ts b/src/commands/sandbox-explain.ts index 70d3b99bf1d..ec6791d4d39 100644 --- a/src/commands/sandbox-explain.ts +++ b/src/commands/sandbox-explain.ts @@ -18,7 +18,10 @@ import { resolveAgentIdFromSessionKey, } from "../routing/session-key.js"; import { type RuntimeEnv, writeRuntimeJson } from "../runtime.js"; -import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js"; +import { + normalizeOptionalLowercaseString, + normalizeStringifiedOptionalString, +} from "../shared/string-coerce.js"; import { formatDocsLink } from "../terminal/links.js"; import { colorize, isRich, theme } from "../terminal/theme.js"; import { INTERNAL_MESSAGE_CHANNEL } from "../utils/message-channel.js"; @@ -181,7 +184,7 @@ export async function sandboxExplainCommand( const agentAllow = channel ? elevatedAgent?.allowFrom?.[channel] : undefined; const allowTokens = (values?: Array) => - (values ?? []).map((v) => String(v).trim()).filter(Boolean); + (values ?? []).map((v) => normalizeStringifiedOptionalString(v) ?? "").filter(Boolean); const globalAllowTokens = allowTokens(globalAllow); const agentAllowTokens = allowTokens(agentAllow); diff --git a/src/commands/status-all/gateway.ts b/src/commands/status-all/gateway.ts index 5baa3e51977..da0c2ad99ef 100644 --- a/src/commands/status-all/gateway.ts +++ b/src/commands/status-all/gateway.ts @@ -1,5 +1,8 @@ import fs from "node:fs/promises"; -import { normalizeLowercaseStringOrEmpty } from "../../shared/string-coerce.js"; +import { + normalizeLowercaseStringOrEmpty, + normalizeOptionalString, +} from "../../shared/string-coerce.js"; export async function readFileTailLines(filePath: string, maxLines: number): Promise { const raw = await fs.readFile(filePath, "utf8").catch(() => ""); @@ -115,8 +118,8 @@ export function summarizeLogTail(rawLines: string[], opts?: { maxLines?: number return null; } })(); - const code = parsed?.error?.code?.trim() || null; - const msg = parsed?.error?.message?.trim() || null; + const code = normalizeOptionalString(parsed?.error?.code) ?? null; + const msg = normalizeOptionalString(parsed?.error?.message) ?? null; const msgShort = msg ? normalizeLowercaseStringOrEmpty(msg).includes("signing in again") ? "re-auth required" @@ -133,7 +136,7 @@ export function summarizeLogTail(rawLines: string[], opts?: { maxLines?: number /^Embedded agent failed before reply:\s+OAuth token refresh failed for ([^:]+):/, ); if (embedded) { - const provider = embedded[1]?.trim() || "unknown"; + const provider = normalizeOptionalString(embedded[1]) || "unknown"; addGroup(`embedded:${provider}`, `Embedded agent: OAuth token refresh failed (${provider})`); continue; } diff --git a/src/commands/status.summary.runtime.ts b/src/commands/status.summary.runtime.ts index cfb27d02664..177d38d6c48 100644 --- a/src/commands/status.summary.runtime.ts +++ b/src/commands/status.summary.runtime.ts @@ -7,6 +7,7 @@ import type { SessionEntry } from "../config/sessions/types.js"; import type { OpenClawConfig } from "../config/types.js"; import { normalizeLowercaseStringOrEmpty, + normalizeOptionalString, normalizeOptionalLowercaseString, } from "../shared/string-coerce.js"; @@ -24,7 +25,7 @@ function resolveStatusModelRefFromRaw(params: { const aliasKey = normalizeLowercaseStringOrEmpty(trimmed); for (const [modelKey, entry] of Object.entries(configuredModels)) { const aliasValue = (entry as { alias?: unknown } | undefined)?.alias; - const alias = typeof aliasValue === "string" ? aliasValue.trim() : ""; + const alias = normalizeOptionalString(aliasValue) ?? ""; if (!alias || normalizeOptionalLowercaseString(alias) !== aliasKey) { continue; } diff --git a/src/commands/tasks.ts b/src/commands/tasks.ts index 0016660c5a8..229c013e346 100644 --- a/src/commands/tasks.ts +++ b/src/commands/tasks.ts @@ -93,9 +93,9 @@ function formatTaskRows(tasks: TaskRecord[], rich: boolean) { const lines = [rich ? theme.heading(header) : header]; for (const task of tasks) { const summary = truncate( - task.terminalSummary?.trim() || - task.progressSummary?.trim() || - task.label?.trim() || + normalizeOptionalString(task.terminalSummary) || + normalizeOptionalString(task.progressSummary) || + normalizeOptionalString(task.label) || task.task.trim(), 80, ); @@ -105,7 +105,7 @@ function formatTaskRows(tasks: TaskRecord[], rich: boolean) { formatTaskStatusCell(task.status, rich), task.deliveryStatus.padEnd(DELIVERY_PAD), shortToken(task.runId, RUN_PAD).padEnd(RUN_PAD), - truncate(task.childSessionKey?.trim() || "n/a", 36).padEnd(36), + truncate(normalizeOptionalString(task.childSessionKey) || "n/a", 36).padEnd(36), summary, ].join(" "); lines.push(line.trimEnd()); diff --git a/src/config/channel-compat-normalization.ts b/src/config/channel-compat-normalization.ts index cc7832ec7c5..0b664caf917 100644 --- a/src/config/channel-compat-normalization.ts +++ b/src/config/channel-compat-normalization.ts @@ -1,3 +1,5 @@ +import { normalizeStringEntries } from "../shared/string-normalization.js"; + type CompatMutationResult = { entry: Record; changed: boolean; @@ -32,8 +34,8 @@ function allowFromListsMatch(left: unknown, right: unknown): boolean { if (!Array.isArray(left) || !Array.isArray(right)) { return false; } - const normalizedLeft = left.map((value) => String(value).trim()).filter(Boolean); - const normalizedRight = right.map((value) => String(value).trim()).filter(Boolean); + const normalizedLeft = normalizeStringEntries(left); + const normalizedRight = normalizeStringEntries(right); if (normalizedLeft.length !== normalizedRight.length) { return false; } diff --git a/src/config/legacy.shared.ts b/src/config/legacy.shared.ts index e97e2d4be70..7a067271cd3 100644 --- a/src/config/legacy.shared.ts +++ b/src/config/legacy.shared.ts @@ -18,6 +18,7 @@ export type LegacyConfigMigrationSpec = LegacyConfigMigration & { }; import { isSafeExecutableValue } from "../infra/exec-safety.js"; +import { normalizeOptionalString } from "../shared/string-coerce.js"; import { isRecord } from "../utils.js"; import { isBlockedObjectKey } from "./prototype-keys.js"; export { isRecord }; @@ -101,23 +102,22 @@ export const resolveDefaultAgentIdFromRaw = (raw: Record) => { isRecord(entry) && entry.default === true && typeof entry.id === "string" && - entry.id.trim() !== "", + normalizeOptionalString(entry.id) !== undefined, ); if (defaultEntry) { - return defaultEntry.id.trim(); + return normalizeOptionalString(defaultEntry.id) ?? "main"; } const routing = getRecord(raw.routing); - const routingDefault = - typeof routing?.defaultAgentId === "string" ? routing.defaultAgentId.trim() : ""; + const routingDefault = normalizeOptionalString(routing?.defaultAgentId) ?? ""; if (routingDefault) { return routingDefault; } const firstEntry = list.find( (entry): entry is { id: string } => - isRecord(entry) && typeof entry.id === "string" && entry.id.trim() !== "", + isRecord(entry) && normalizeOptionalString(entry.id) !== undefined, ); if (firstEntry) { - return firstEntry.id.trim(); + return normalizeOptionalString(firstEntry.id) ?? "main"; } return "main"; }; diff --git a/src/config/plugin-auto-enable.prefer-over.ts b/src/config/plugin-auto-enable.prefer-over.ts index d938b011a27..11cdda2ce2c 100644 --- a/src/config/plugin-auto-enable.prefer-over.ts +++ b/src/config/plugin-auto-enable.prefer-over.ts @@ -2,6 +2,8 @@ import fs from "node:fs"; import path from "node:path"; import { getChatChannelMeta, normalizeChatChannelId } from "../channels/registry.js"; import type { PluginManifestRegistry } from "../plugins/manifest-registry.js"; +import { normalizeOptionalString } from "../shared/string-coerce.js"; +import { normalizeStringEntries } from "../shared/string-normalization.js"; import { isRecord, resolveConfigDir, resolveUserPath } from "../utils.js"; import type { OpenClawConfig } from "./config.js"; import type { PluginAutoEnableCandidate } from "./plugin-auto-enable.shared.js"; @@ -14,21 +16,19 @@ type ExternalCatalogChannelEntry = { const ENV_CATALOG_PATHS = ["OPENCLAW_PLUGIN_CATALOG_PATHS", "OPENCLAW_MPM_CATALOG_PATHS"]; function splitEnvPaths(value: string): string[] { - const trimmed = value.trim(); + const trimmed = normalizeOptionalString(value) ?? ""; if (!trimmed) { return []; } - return trimmed - .split(/[;,]/g) - .flatMap((chunk) => chunk.split(path.delimiter)) - .map((entry) => entry.trim()) - .filter(Boolean); + return normalizeStringEntries( + trimmed.split(/[;,]/g).flatMap((chunk) => chunk.split(path.delimiter)), + ); } function resolveExternalCatalogPaths(env: NodeJS.ProcessEnv): string[] { for (const key of ENV_CATALOG_PATHS) { - const raw = env[key]; - if (raw && raw.trim()) { + const raw = normalizeOptionalString(env[key]); + if (raw) { return splitEnvPaths(raw); } } @@ -58,7 +58,7 @@ function parseExternalCatalogChannelEntries(raw: unknown): ExternalCatalogChanne continue; } const channel = entry.openclaw.channel; - const id = typeof channel.id === "string" ? channel.id.trim() : ""; + const id = normalizeOptionalString(channel.id) ?? ""; if (!id) { continue; } diff --git a/src/config/runtime-group-policy.ts b/src/config/runtime-group-policy.ts index 62ee6db7d8a..d8b03fb3553 100644 --- a/src/config/runtime-group-policy.ts +++ b/src/config/runtime-group-policy.ts @@ -1,3 +1,4 @@ +import { normalizeOptionalString } from "../shared/string-coerce.js"; import type { GroupPolicy } from "./types.base.js"; export type RuntimeGroupPolicyResolution = { @@ -103,7 +104,7 @@ export function warnMissingProviderGroupPolicyFallbackOnce(params: { return false; } warnedMissingProviderGroupPolicy.add(key); - const blockedLabel = params.blockedLabel?.trim() || "group messages"; + const blockedLabel = normalizeOptionalString(params.blockedLabel) || "group messages"; params.log( `${params.providerKey}: channels.${params.providerKey} is missing; defaulting groupPolicy to "allowlist" (${blockedLabel} blocked until explicitly configured).`, ); diff --git a/src/config/sessions/group.ts b/src/config/sessions/group.ts index 8a2e58c3fdb..ad3952ac075 100644 --- a/src/config/sessions/group.ts +++ b/src/config/sessions/group.ts @@ -45,7 +45,7 @@ function normalizeGroupLabel(raw?: string) { } function shortenGroupId(value?: string) { - const trimmed = value?.trim() ?? ""; + const trimmed = normalizeOptionalString(value) ?? ""; if (!trimmed) { return ""; } diff --git a/src/config/sessions/store-maintenance.ts b/src/config/sessions/store-maintenance.ts index 410fcbc00f0..2c7c0846aae 100644 --- a/src/config/sessions/store-maintenance.ts +++ b/src/config/sessions/store-maintenance.ts @@ -3,6 +3,7 @@ import path from "node:path"; import { parseByteSize } from "../../cli/parse-bytes.js"; import { parseDurationMs } from "../../cli/parse-duration.js"; import { createSubsystemLogger } from "../../logging/subsystem.js"; +import { normalizeStringifiedOptionalString } from "../../shared/string-coerce.js"; import { loadConfig } from "../config.js"; import type { SessionMaintenanceConfig, SessionMaintenanceMode } from "../types.base.js"; import type { SessionEntry } from "./types.js"; @@ -37,11 +38,12 @@ export type ResolvedSessionMaintenanceConfig = { function resolvePruneAfterMs(maintenance?: SessionMaintenanceConfig): number { const raw = maintenance?.pruneAfter ?? maintenance?.pruneDays; - if (raw === undefined || raw === null || raw === "") { + const normalized = normalizeStringifiedOptionalString(raw); + if (!normalized) { return DEFAULT_SESSION_PRUNE_AFTER_MS; } try { - return parseDurationMs(String(raw).trim(), { defaultUnit: "d" }); + return parseDurationMs(normalized, { defaultUnit: "d" }); } catch { return DEFAULT_SESSION_PRUNE_AFTER_MS; } @@ -49,11 +51,12 @@ function resolvePruneAfterMs(maintenance?: SessionMaintenanceConfig): number { function resolveRotateBytes(maintenance?: SessionMaintenanceConfig): number { const raw = maintenance?.rotateBytes; - if (raw === undefined || raw === null || raw === "") { + const normalized = normalizeStringifiedOptionalString(raw); + if (!normalized) { return DEFAULT_SESSION_ROTATE_BYTES; } try { - return parseByteSize(String(raw).trim(), { defaultUnit: "b" }); + return parseByteSize(normalized, { defaultUnit: "b" }); } catch { return DEFAULT_SESSION_ROTATE_BYTES; } @@ -67,11 +70,12 @@ function resolveResetArchiveRetentionMs( if (raw === false) { return null; } - if (raw === undefined || raw === null || raw === "") { + const normalized = normalizeStringifiedOptionalString(raw); + if (!normalized) { return pruneAfterMs; } try { - return parseDurationMs(String(raw).trim(), { defaultUnit: "d" }); + return parseDurationMs(normalized, { defaultUnit: "d" }); } catch { return pruneAfterMs; } @@ -79,11 +83,12 @@ function resolveResetArchiveRetentionMs( function resolveMaxDiskBytes(maintenance?: SessionMaintenanceConfig): number | null { const raw = maintenance?.maxDiskBytes; - if (raw === undefined || raw === null || raw === "") { + const normalized = normalizeStringifiedOptionalString(raw); + if (!normalized) { return null; } try { - return parseByteSize(String(raw).trim(), { defaultUnit: "b" }); + return parseByteSize(normalized, { defaultUnit: "b" }); } catch { return null; } @@ -112,11 +117,12 @@ function resolveHighWaterBytes( return null; } const raw = maintenance?.highWaterBytes; - if (raw === undefined || raw === null || raw === "") { + const normalized = normalizeStringifiedOptionalString(raw); + if (!normalized) { return computeDefault(); } try { - const parsed = parseByteSize(String(raw).trim(), { defaultUnit: "b" }); + const parsed = parseByteSize(normalized, { defaultUnit: "b" }); return Math.min(parsed, maxDiskBytes); } catch { return computeDefault(); diff --git a/src/config/zod-schema.agent-runtime.ts b/src/config/zod-schema.agent-runtime.ts index 8553f30e448..e002658c7ce 100644 --- a/src/config/zod-schema.agent-runtime.ts +++ b/src/config/zod-schema.agent-runtime.ts @@ -1,7 +1,10 @@ import { z } from "zod"; import { getBlockedNetworkModeReason } from "../agents/sandbox/network-mode.js"; import { parseDurationMs } from "../cli/parse-duration.js"; -import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js"; +import { + normalizeLowercaseStringOrEmpty, + normalizeOptionalString, +} from "../shared/string-coerce.js"; import { AgentModelSchema } from "./zod-schema.agent-model.js"; import { GroupChatSchema, @@ -142,7 +145,7 @@ export const SandboxDockerSchema = z .superRefine((data, ctx) => { if (data.binds) { for (let i = 0; i < data.binds.length; i += 1) { - const bind = data.binds[i]?.trim() ?? ""; + const bind = normalizeOptionalString(data.binds[i]) ?? ""; if (!bind) { ctx.addIssue({ code: z.ZodIssueCode.custom, diff --git a/src/config/zod-schema.agents.ts b/src/config/zod-schema.agents.ts index 022eed3c920..05dd217a4aa 100644 --- a/src/config/zod-schema.agents.ts +++ b/src/config/zod-schema.agents.ts @@ -1,4 +1,5 @@ import { z } from "zod"; +import { normalizeOptionalString } from "../shared/string-coerce.js"; import { AgentDefaultsSchema } from "./zod-schema.agent-defaults.js"; import { AgentEntrySchema } from "./zod-schema.agent-runtime.js"; import { TranscribeAudioSchema } from "./zod-schema.core.js"; @@ -61,7 +62,7 @@ const AcpBindingSchema = z }) .strict() .superRefine((value, ctx) => { - const peerId = value.match.peer?.id?.trim() ?? ""; + const peerId = normalizeOptionalString(value.match.peer?.id) ?? ""; if (!peerId) { ctx.addIssue({ code: z.ZodIssueCode.custom, diff --git a/src/config/zod-schema.core.ts b/src/config/zod-schema.core.ts index 7ef6db117ce..93b568b15cc 100644 --- a/src/config/zod-schema.core.ts +++ b/src/config/zod-schema.core.ts @@ -6,6 +6,7 @@ import { isValidExecSecretRefId, isValidFileSecretRefId, } from "../secrets/ref-contract.js"; +import { normalizeStringEntries } from "../shared/string-normalization.js"; import type { ModelCompatConfig } from "./types.models.js"; import { MODEL_APIS } from "./types.models.js"; import { createAllowDenyChannelRulesSchema } from "./zod-schema.allowdeny.js"; @@ -558,7 +559,7 @@ export const CliBackendSchema = z .strict(); export const normalizeAllowFrom = (values?: Array): string[] => - (values ?? []).map((v) => String(v).trim()).filter(Boolean); + normalizeStringEntries(values); export const requireOpenAllowFrom = (params: { policy?: string; diff --git a/src/config/zod-schema.providers-core.ts b/src/config/zod-schema.providers-core.ts index 6d1e31a70b4..b54ddb42c36 100644 --- a/src/config/zod-schema.providers-core.ts +++ b/src/config/zod-schema.providers-core.ts @@ -6,6 +6,7 @@ import { normalizeTelegramCommandName, resolveTelegramCustomCommands, } from "../plugin-sdk/telegram-command-config.js"; +import { normalizeOptionalString } from "../shared/string-coerce.js"; import { ToolPolicySchema } from "./zod-schema.agent-runtime.js"; import { ChannelHealthMonitorSchema, @@ -645,10 +646,10 @@ export const DiscordAccountSchema = z }) .strict() .superRefine((value, ctx) => { - const activityText = typeof value.activity === "string" ? value.activity.trim() : ""; + const activityText = normalizeOptionalString(value.activity) ?? ""; const hasActivity = Boolean(activityText); const hasActivityType = value.activityType !== undefined; - const activityUrl = typeof value.activityUrl === "string" ? value.activityUrl.trim() : ""; + const activityUrl = normalizeOptionalString(value.activityUrl) ?? ""; const hasActivityUrl = Boolean(activityUrl); if ((hasActivityType || hasActivityUrl) && !hasActivity) { diff --git a/src/config/zod-schema.providers-whatsapp.ts b/src/config/zod-schema.providers-whatsapp.ts index 77395cc1ae0..1ff2b001981 100644 --- a/src/config/zod-schema.providers-whatsapp.ts +++ b/src/config/zod-schema.providers-whatsapp.ts @@ -1,4 +1,5 @@ import { z } from "zod"; +import { normalizeStringEntries } from "../shared/string-normalization.js"; import { ToolPolicySchema } from "./zod-schema.agent-runtime.js"; import { ChannelHealthMonitorSchema, @@ -75,9 +76,7 @@ function enforceOpenDmPolicyAllowFromStar(params: { if (params.dmPolicy !== "open") { return; } - const allow = (Array.isArray(params.allowFrom) ? params.allowFrom : []) - .map((v) => String(v).trim()) - .filter(Boolean); + const allow = normalizeStringEntries(Array.isArray(params.allowFrom) ? params.allowFrom : []); if (allow.includes("*")) { return; } @@ -98,9 +97,7 @@ function enforceAllowlistDmPolicyAllowFrom(params: { if (params.dmPolicy !== "allowlist") { return; } - const allow = (Array.isArray(params.allowFrom) ? params.allowFrom : []) - .map((v) => String(v).trim()) - .filter(Boolean); + const allow = normalizeStringEntries(Array.isArray(params.allowFrom) ? params.allowFrom : []); if (allow.length > 0) { return; } diff --git a/src/config/zod-schema.secret-input-validation.ts b/src/config/zod-schema.secret-input-validation.ts index 3426e61d15f..7dc5ff61f42 100644 --- a/src/config/zod-schema.secret-input-validation.ts +++ b/src/config/zod-schema.secret-input-validation.ts @@ -1,4 +1,5 @@ import { z } from "zod"; +import { normalizeOptionalString } from "../shared/string-coerce.js"; import { hasConfiguredSecretInput } from "./types.secrets.js"; type TelegramAccountLike = { @@ -44,7 +45,7 @@ export function validateTelegramWebhookSecretRequirements( value: TelegramConfigLike, ctx: z.RefinementCtx, ): void { - const baseWebhookUrl = typeof value.webhookUrl === "string" ? value.webhookUrl.trim() : ""; + const baseWebhookUrl = normalizeOptionalString(value.webhookUrl) ?? ""; const hasBaseWebhookSecret = hasConfiguredSecretInput(value.webhookSecret); if (baseWebhookUrl && !hasBaseWebhookSecret) { ctx.addIssue({ @@ -54,8 +55,7 @@ export function validateTelegramWebhookSecretRequirements( }); } forEachEnabledAccount(value.accounts, (accountId, account) => { - const accountWebhookUrl = - typeof account.webhookUrl === "string" ? account.webhookUrl.trim() : ""; + const accountWebhookUrl = normalizeOptionalString(account.webhookUrl) ?? ""; if (!accountWebhookUrl) { return; } diff --git a/src/config/zod-schema.session.ts b/src/config/zod-schema.session.ts index 3f4b6a24d80..a51c53fe86c 100644 --- a/src/config/zod-schema.session.ts +++ b/src/config/zod-schema.session.ts @@ -1,6 +1,7 @@ import { z } from "zod"; import { parseByteSize } from "../cli/parse-bytes.js"; import { parseDurationMs } from "../cli/parse-duration.js"; +import { normalizeStringifiedOptionalString } from "../shared/string-coerce.js"; import { ElevatedAllowFromSchema } from "./zod-schema.agent-runtime.js"; import { createAllowDenyChannelRulesSchema } from "./zod-schema.allowdeny.js"; import { @@ -85,7 +86,9 @@ export const SessionSchema = z .superRefine((val, ctx) => { if (val.pruneAfter !== undefined) { try { - parseDurationMs(String(val.pruneAfter).trim(), { defaultUnit: "d" }); + parseDurationMs(normalizeStringifiedOptionalString(val.pruneAfter) ?? "", { + defaultUnit: "d", + }); } catch { ctx.addIssue({ code: z.ZodIssueCode.custom, @@ -96,7 +99,9 @@ export const SessionSchema = z } if (val.rotateBytes !== undefined) { try { - parseByteSize(String(val.rotateBytes).trim(), { defaultUnit: "b" }); + parseByteSize(normalizeStringifiedOptionalString(val.rotateBytes) ?? "", { + defaultUnit: "b", + }); } catch { ctx.addIssue({ code: z.ZodIssueCode.custom, @@ -107,7 +112,9 @@ export const SessionSchema = z } if (val.resetArchiveRetention !== undefined && val.resetArchiveRetention !== false) { try { - parseDurationMs(String(val.resetArchiveRetention).trim(), { defaultUnit: "d" }); + parseDurationMs(normalizeStringifiedOptionalString(val.resetArchiveRetention) ?? "", { + defaultUnit: "d", + }); } catch { ctx.addIssue({ code: z.ZodIssueCode.custom, @@ -118,7 +125,9 @@ export const SessionSchema = z } if (val.maxDiskBytes !== undefined) { try { - parseByteSize(String(val.maxDiskBytes).trim(), { defaultUnit: "b" }); + parseByteSize(normalizeStringifiedOptionalString(val.maxDiskBytes) ?? "", { + defaultUnit: "b", + }); } catch { ctx.addIssue({ code: z.ZodIssueCode.custom, @@ -129,7 +138,9 @@ export const SessionSchema = z } if (val.highWaterBytes !== undefined) { try { - parseByteSize(String(val.highWaterBytes).trim(), { defaultUnit: "b" }); + parseByteSize(normalizeStringifiedOptionalString(val.highWaterBytes) ?? "", { + defaultUnit: "b", + }); } catch { ctx.addIssue({ code: z.ZodIssueCode.custom, diff --git a/src/config/zod-schema.ts b/src/config/zod-schema.ts index 47c868dd0db..937c5b427d5 100644 --- a/src/config/zod-schema.ts +++ b/src/config/zod-schema.ts @@ -1,7 +1,10 @@ import { z } from "zod"; import { parseByteSize } from "../cli/parse-bytes.js"; import { parseDurationMs } from "../cli/parse-duration.js"; -import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js"; +import { + normalizeLowercaseStringOrEmpty, + normalizeStringifiedOptionalString, +} from "../shared/string-coerce.js"; import { ToolsSchema } from "./zod-schema.agent-runtime.js"; import { AgentsSchema, AudioSchema, BindingsSchema, BroadcastSchema } from "./zod-schema.agents.js"; import { ApprovalsSchema } from "./zod-schema.approvals.js"; @@ -568,7 +571,9 @@ export const OpenClawSchema = z .superRefine((val, ctx) => { if (val.sessionRetention !== undefined && val.sessionRetention !== false) { try { - parseDurationMs(String(val.sessionRetention).trim(), { defaultUnit: "h" }); + parseDurationMs(normalizeStringifiedOptionalString(val.sessionRetention) ?? "", { + defaultUnit: "h", + }); } catch { ctx.addIssue({ code: z.ZodIssueCode.custom, @@ -579,7 +584,9 @@ export const OpenClawSchema = z } if (val.runLog?.maxBytes !== undefined) { try { - parseByteSize(String(val.runLog.maxBytes).trim(), { defaultUnit: "b" }); + parseByteSize(normalizeStringifiedOptionalString(val.runLog.maxBytes) ?? "", { + defaultUnit: "b", + }); } catch { ctx.addIssue({ code: z.ZodIssueCode.custom,