diff --git a/src/agents/agent-command.ts b/src/agents/agent-command.ts index ff718d4506a..6335205a4d0 100644 --- a/src/agents/agent-command.ts +++ b/src/agents/agent-command.ts @@ -36,6 +36,7 @@ import { defaultRuntime, type RuntimeEnv } from "../runtime.js"; import { applyVerboseOverride } from "../sessions/level-overrides.js"; import { applyModelOverrideToSessionEntry } from "../sessions/model-overrides.js"; import { resolveSendPolicy } from "../sessions/send-policy.js"; +import { normalizeOptionalString } from "../shared/string-coerce.js"; import { sanitizeForLog } from "../terminal/ansi.js"; import { resolveMessageChannel } from "../utils/message-channel.js"; import { @@ -252,7 +253,7 @@ async function prepareAgentCommandExecution( throw new Error('Invalid verbose level. Use "on", "full", or "off".'); } - const laneRaw = typeof opts.lane === "string" ? opts.lane.trim() : ""; + const laneRaw = normalizeOptionalString(opts.lane) ?? ""; const isSubagentLane = laneRaw === String(AGENT_LANE_SUBAGENT); const timeoutSecondsRaw = opts.timeout !== undefined diff --git a/src/agents/auth-profiles/state.ts b/src/agents/auth-profiles/state.ts index 823d64264c2..90fc8291277 100644 --- a/src/agents/auth-profiles/state.ts +++ b/src/agents/auth-profiles/state.ts @@ -1,5 +1,6 @@ import fs from "node:fs"; import { loadJsonFile, saveJsonFile } from "../../infra/json-file.js"; +import { normalizeOptionalString } from "../../shared/string-coerce.js"; import { AUTH_STORE_VERSION } from "./constants.js"; import { resolveAuthStatePath } from "./paths.js"; import type { AuthProfileState, AuthProfileStateStore, ProfileUsageStats } from "./types.js"; @@ -13,9 +14,7 @@ function normalizeAuthProfileOrder(raw: unknown): AuthProfileState["order"] { if (!Array.isArray(value)) { return acc; } - const list = value - .map((entry) => (typeof entry === "string" ? entry.trim() : "")) - .filter(Boolean); + const list = value.map((entry) => normalizeOptionalString(entry) ?? "").filter(Boolean); if (list.length > 0) { acc[provider] = list; } diff --git a/src/agents/bootstrap-files.ts b/src/agents/bootstrap-files.ts index 401531e0cc8..5bc5cc889c9 100644 --- a/src/agents/bootstrap-files.ts +++ b/src/agents/bootstrap-files.ts @@ -1,6 +1,7 @@ import fs from "node:fs/promises"; import type { OpenClawConfig } from "../config/config.js"; import type { AgentContextInjection } from "../config/types.agent-defaults.js"; +import { normalizeOptionalString } from "../shared/string-coerce.js"; import { resolveAgentConfig, resolveSessionAgentIds } from "./agent-scope.js"; import { getOrLoadBootstrapFiles } from "./bootstrap-cache.js"; import { applyBootstrapHookOverrides } from "./bootstrap-hooks.js"; @@ -115,7 +116,7 @@ function sanitizeBootstrapFiles( ): WorkspaceBootstrapFile[] { const sanitized: WorkspaceBootstrapFile[] = []; for (const file of files) { - const pathValue = typeof file.path === "string" ? file.path.trim() : ""; + const pathValue = normalizeOptionalString(file.path) ?? ""; if (!pathValue) { warn?.( `skipping bootstrap file "${file.name}" — missing or invalid "path" field (hook may have used "filePath" instead)`, diff --git a/src/agents/mcp-transport.ts b/src/agents/mcp-transport.ts index 04c059f3f05..6c35f9710d3 100644 --- a/src/agents/mcp-transport.ts +++ b/src/agents/mcp-transport.ts @@ -7,6 +7,7 @@ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/ import type { FetchLike, Transport } from "@modelcontextprotocol/sdk/shared/transport.js"; import { loadUndiciRuntimeDeps } from "../infra/net/undici-runtime.js"; import { logDebug } from "../logger.js"; +import { normalizeOptionalString } from "../shared/string-coerce.js"; import { resolveMcpTransportConfig } from "./mcp-transport-config.js"; export type ResolvedMcpTransport = { @@ -23,7 +24,8 @@ function attachStderrLogging(serverName: string, transport: StdioClientTransport return undefined; } const onData = (chunk: Buffer | string) => { - const message = String(chunk).trim(); + const message = + normalizeOptionalString(typeof chunk === "string" ? chunk : String(chunk)) ?? ""; if (!message) { return; } diff --git a/src/agents/models-config.merge.ts b/src/agents/models-config.merge.ts index 04654f773b8..38bedf4615f 100644 --- a/src/agents/models-config.merge.ts +++ b/src/agents/models-config.merge.ts @@ -1,3 +1,4 @@ +import { normalizeOptionalString } from "../shared/string-coerce.js"; import { isNonSecretApiKeyMarker } from "./model-auth-markers.js"; import type { ProviderConfig } from "./models-config.providers.secrets.js"; @@ -30,7 +31,7 @@ function getProviderModelId(model: unknown): string { return ""; } const id = (model as { id?: unknown }).id; - return typeof id === "string" ? id.trim() : ""; + return normalizeOptionalString(id) ?? ""; } export function mergeProviderModels( @@ -136,7 +137,7 @@ export function mergeProviders(params: { }): Record { const out: Record = params.implicit ? { ...params.implicit } : {}; for (const [key, explicit] of Object.entries(params.explicit ?? {})) { - const providerKey = key.trim(); + const providerKey = normalizeOptionalString(key) ?? ""; if (!providerKey) { continue; } @@ -147,11 +148,7 @@ export function mergeProviders(params: { } function resolveProviderApi(entry: { api?: unknown } | undefined): string | undefined { - if (typeof entry?.api !== "string") { - return undefined; - } - const api = entry.api.trim(); - return api || undefined; + return normalizeOptionalString(entry?.api); } function resolveModelApiSurface(entry: { models?: unknown } | undefined): string | undefined { @@ -165,7 +162,8 @@ function resolveModelApiSurface(entry: { models?: unknown } | undefined): string return []; } const api = (model as { api?: unknown }).api; - return typeof api === "string" && api.trim() ? [api.trim()] : []; + const normalized = normalizeOptionalString(api); + return normalized ? [normalized] : []; }) .toSorted(); diff --git a/src/agents/models-config.providers.policy.lookup.ts b/src/agents/models-config.providers.policy.lookup.ts index 0965c1082a2..214acc0bc7f 100644 --- a/src/agents/models-config.providers.policy.lookup.ts +++ b/src/agents/models-config.providers.policy.lookup.ts @@ -1,4 +1,5 @@ import { MODEL_APIS } from "../config/types.models.js"; +import { normalizeOptionalString } from "../shared/string-coerce.js"; import type { ProviderConfig } from "./models-config.providers.secrets.js"; const GENERIC_PROVIDER_APIS = new Set([ @@ -12,7 +13,7 @@ export function resolveProviderPluginLookupKey( providerKey: string, provider?: ProviderConfig, ): string { - const api = typeof provider?.api === "string" ? provider.api.trim() : ""; + const api = normalizeOptionalString(provider?.api) ?? ""; if ( api && MODEL_APIS.includes(api as (typeof MODEL_APIS)[number]) && diff --git a/src/agents/pi-embedded-helpers/bootstrap.ts b/src/agents/pi-embedded-helpers/bootstrap.ts index e6e0792f4ba..858c8722b51 100644 --- a/src/agents/pi-embedded-helpers/bootstrap.ts +++ b/src/agents/pi-embedded-helpers/bootstrap.ts @@ -2,6 +2,7 @@ import fs from "node:fs/promises"; import path from "node:path"; import type { AgentMessage } from "@mariozechner/pi-agent-core"; import type { OpenClawConfig } from "../../config/config.js"; +import { normalizeOptionalString } from "../../shared/string-coerce.js"; import { truncateUtf16Safe } from "../../utils.js"; import type { WorkspaceBootstrapFile } from "../workspace.js"; import type { EmbeddedContextFile } from "./types.js"; @@ -210,7 +211,7 @@ export function buildBootstrapContextFiles( if (remainingTotalChars <= 0) { break; } - const pathValue = typeof file.path === "string" ? file.path.trim() : ""; + const pathValue = normalizeOptionalString(file.path) ?? ""; if (!pathValue) { opts?.warn?.( `skipping bootstrap file "${file.name}" — missing or invalid "path" field (hook may have used "filePath" instead)`, diff --git a/src/agents/pi-embedded-runner/anthropic-family-tool-payload-compat.ts b/src/agents/pi-embedded-runner/anthropic-family-tool-payload-compat.ts index 249084a3488..398dc96c910 100644 --- a/src/agents/pi-embedded-runner/anthropic-family-tool-payload-compat.ts +++ b/src/agents/pi-embedded-runner/anthropic-family-tool-payload-compat.ts @@ -1,5 +1,6 @@ import type { StreamFn } from "@mariozechner/pi-agent-core"; import { streamSimple } from "@mariozechner/pi-ai"; +import { normalizeOptionalString } from "../../shared/string-coerce.js"; type AnthropicToolSchemaMode = "openai-functions"; type AnthropicToolChoiceMode = "openai-string-modes"; @@ -70,7 +71,7 @@ function normalizeOpenAiFunctionAnthropicToolDefinition( return toolObj; } - const rawName = typeof toolObj.name === "string" ? toolObj.name.trim() : ""; + const rawName = normalizeOptionalString(toolObj.name) ?? ""; if (!rawName) { return toolObj; } diff --git a/src/agents/pi-embedded-runner/google-prompt-cache.ts b/src/agents/pi-embedded-runner/google-prompt-cache.ts index 09ae3583a29..5dd2a1ddbd5 100644 --- a/src/agents/pi-embedded-runner/google-prompt-cache.ts +++ b/src/agents/pi-embedded-runner/google-prompt-cache.ts @@ -3,6 +3,7 @@ import type { StreamFn } from "@mariozechner/pi-agent-core"; import type { Api, Model } from "@mariozechner/pi-ai"; import { parseGeminiAuth } from "../../infra/gemini-auth.js"; import { normalizeGoogleApiBaseUrl } from "../../infra/google-api-base-url.js"; +import { normalizeOptionalString } from "../../shared/string-coerce.js"; import { buildGuardedModelFetch } from "../provider-transport-fetch.js"; import { stableStringify } from "../stable-stringify.js"; import { stripSystemPromptCacheBoundary } from "../system-prompt-cache-boundary.js"; @@ -226,7 +227,7 @@ async function createGooglePromptCache(params: { return null; } const json = (await response.json()) as { name?: string; expireTime?: string }; - const cachedContent = typeof json.name === "string" ? json.name.trim() : ""; + const cachedContent = normalizeOptionalString(json.name) ?? ""; return cachedContent ? { cachedContent, expireTime: json.expireTime } : null; } diff --git a/src/agents/pi-embedded-runner/run/attempt.ts b/src/agents/pi-embedded-runner/run/attempt.ts index 38c23261368..5476a0a3344 100644 --- a/src/agents/pi-embedded-runner/run/attempt.ts +++ b/src/agents/pi-embedded-runner/run/attempt.ts @@ -27,6 +27,7 @@ import { resolveToolCallArgumentsEncoding } from "../../../plugins/provider-mode import { resolveProviderSystemPromptContribution } from "../../../plugins/provider-runtime.js"; import { isSubagentSessionKey } from "../../../routing/session-key.js"; import { normalizeOptionalLowercaseString } from "../../../shared/string-coerce.js"; +import { normalizeOptionalString } from "../../../shared/string-coerce.js"; import { buildTtsSystemPromptHint } from "../../../tts/tts.js"; import { resolveUserPath } from "../../../utils.js"; import { normalizeMessageChannel } from "../../../utils/message-channel.js"; @@ -1654,8 +1655,7 @@ export async function runEmbeddedAttempt( `hooks: prepended context to prompt (${hookResult.prependContext.length} chars)`, ); } - const legacySystemPrompt = - typeof hookResult?.systemPrompt === "string" ? hookResult.systemPrompt.trim() : ""; + const legacySystemPrompt = normalizeOptionalString(hookResult?.systemPrompt) ?? ""; if (legacySystemPrompt) { applySystemPromptOverrideToSession(activeSession, legacySystemPrompt); systemPromptText = legacySystemPrompt; diff --git a/src/agents/pi-embedded-subscribe.ts b/src/agents/pi-embedded-subscribe.ts index 4288c727908..58651f869d8 100644 --- a/src/agents/pi-embedded-subscribe.ts +++ b/src/agents/pi-embedded-subscribe.ts @@ -8,6 +8,7 @@ import { emitAgentEvent } from "../infra/agent-events.js"; import { createSubsystemLogger } from "../logging/subsystem.js"; import type { InlineCodeState } from "../markdown/code-spans.js"; import { buildCodeSpanIndex, createInlineCodeState } from "../markdown/code-spans.js"; +import { normalizeOptionalString } from "../shared/string-coerce.js"; import { EmbeddedBlockChunker } from "./pi-embedded-block-chunker.js"; import { isMessagingToolDuplicateNormalized, @@ -43,7 +44,7 @@ function collectPendingMediaFromInternalEvents( continue; } for (const mediaUrl of event.mediaUrls) { - const normalized = typeof mediaUrl === "string" ? mediaUrl.trim() : ""; + const normalized = normalizeOptionalString(mediaUrl) ?? ""; if (!normalized || seen.has(normalized)) { continue; } diff --git a/src/agents/skills.ts b/src/agents/skills.ts index c9c65449f5a..efa5cd945b9 100644 --- a/src/agents/skills.ts +++ b/src/agents/skills.ts @@ -1,5 +1,8 @@ import type { OpenClawConfig } from "../config/config.js"; -import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js"; +import { + normalizeLowercaseStringOrEmpty, + normalizeOptionalString, +} from "../shared/string-coerce.js"; import type { SkillsInstallPreferences } from "./skills/types.js"; export { @@ -38,8 +41,7 @@ export { buildWorkspaceSkillCommandSpecs } from "./skills/command-specs.js"; export function resolveSkillsInstallPreferences(config?: OpenClawConfig): SkillsInstallPreferences { const raw = config?.skills?.install; const preferBrew = raw?.preferBrew ?? true; - const managerRaw = typeof raw?.nodeManager === "string" ? raw.nodeManager.trim() : ""; - const manager = normalizeLowercaseStringOrEmpty(managerRaw); + const manager = normalizeLowercaseStringOrEmpty(normalizeOptionalString(raw?.nodeManager)); const nodeManager: SkillsInstallPreferences["nodeManager"] = manager === "pnpm" || manager === "yarn" || manager === "bun" || manager === "npm" ? manager diff --git a/src/agents/skills/refresh.ts b/src/agents/skills/refresh.ts index f6293e0becd..e8e571e01ad 100644 --- a/src/agents/skills/refresh.ts +++ b/src/agents/skills/refresh.ts @@ -3,6 +3,7 @@ import path from "node:path"; import chokidar, { type FSWatcher } from "chokidar"; import type { OpenClawConfig } from "../../config/config.js"; import { createSubsystemLogger } from "../../logging/subsystem.js"; +import { normalizeOptionalString } from "../../shared/string-coerce.js"; import { CONFIG_DIR, resolveUserPath } from "../../utils.js"; import { resolvePluginSkillDirs } from "./plugin-skills.js"; import { @@ -62,7 +63,7 @@ function resolveWatchPaths(workspaceDir: string, config?: OpenClawConfig): strin paths.push(path.join(os.homedir(), ".agents", "skills")); const extraDirsRaw = config?.skills?.load?.extraDirs ?? []; const extraDirs = extraDirsRaw - .map((d) => (typeof d === "string" ? d.trim() : "")) + .map((d) => normalizeOptionalString(d) ?? "") .filter(Boolean) .map((dir) => resolveUserPath(dir)); paths.push(...extraDirs); diff --git a/src/agents/skills/workspace.ts b/src/agents/skills/workspace.ts index 2b51a6d80b1..303b281c3fc 100644 --- a/src/agents/skills/workspace.ts +++ b/src/agents/skills/workspace.ts @@ -4,6 +4,7 @@ import path from "node:path"; import type { OpenClawConfig } from "../../config/config.js"; import { isPathInside } from "../../infra/path-guards.js"; import { createSubsystemLogger } from "../../logging/subsystem.js"; +import { normalizeOptionalString } from "../../shared/string-coerce.js"; import { CONFIG_DIR, resolveUserPath } from "../../utils.js"; import { resolveSandboxPath } from "../sandbox-paths.js"; import { resolveEffectiveAgentSkillFilter } from "./agent-filter.js"; @@ -410,9 +411,7 @@ function loadSkillEntries( const workspaceSkillsDir = path.resolve(workspaceDir, "skills"); const bundledSkillsDir = opts?.bundledSkillsDir ?? resolveBundledSkillsDir(); const extraDirsRaw = opts?.config?.skills?.load?.extraDirs ?? []; - const extraDirs = extraDirsRaw - .map((d) => (typeof d === "string" ? d.trim() : "")) - .filter(Boolean); + const extraDirs = extraDirsRaw.map((d) => normalizeOptionalString(d) ?? "").filter(Boolean); const pluginSkillDirs = resolvePluginSkillDirs({ workspaceDir, config: opts?.config, diff --git a/src/agents/subagent-announce.ts b/src/agents/subagent-announce.ts index 9f75dda1089..d1a49bc8f6f 100644 --- a/src/agents/subagent-announce.ts +++ b/src/agents/subagent-announce.ts @@ -282,7 +282,7 @@ async function wakeSubagentRunAfterDescendants(params: { timeoutMs: announceTimeoutMs, }), }); - wakeRunId = typeof wakeResponse?.runId === "string" ? wakeResponse.runId.trim() : ""; + wakeRunId = normalizeOptionalString(wakeResponse?.runId) ?? ""; } catch { return false; } diff --git a/src/agents/subagent-attachments.ts b/src/agents/subagent-attachments.ts index d8093dd3fab..b50fb741f41 100644 --- a/src/agents/subagent-attachments.ts +++ b/src/agents/subagent-attachments.ts @@ -2,6 +2,7 @@ import crypto from "node:crypto"; import { promises as fs } from "node:fs"; import path from "node:path"; import type { OpenClawConfig } from "../config/config.js"; +import { normalizeOptionalString } from "../shared/string-coerce.js"; import { resolveAgentWorkspaceDir } from "./agent-scope.js"; export function decodeStrictBase64(value: string, maxDecodedBytes: number): Buffer | null { @@ -137,9 +138,9 @@ export async function materializeSubagentAttachments(params: { let totalBytes = 0; for (const raw of requestedAttachments) { - const name = typeof raw?.name === "string" ? raw.name.trim() : ""; + const name = normalizeOptionalString(raw?.name) ?? ""; const contentVal = typeof raw?.content === "string" ? raw.content : ""; - const encodingRaw = typeof raw?.encoding === "string" ? raw.encoding.trim() : "utf8"; + const encodingRaw = normalizeOptionalString(raw?.encoding) ?? "utf8"; const encoding = encodingRaw === "base64" ? "base64" : "utf8"; if (!name) { diff --git a/src/agents/tools/gateway.ts b/src/agents/tools/gateway.ts index fdcb55ae830..7fb752f4694 100644 --- a/src/agents/tools/gateway.ts +++ b/src/agents/tools/gateway.ts @@ -6,7 +6,10 @@ import { type OperatorScope, } from "../../gateway/method-scopes.js"; import { formatErrorMessage } from "../../infra/errors.js"; -import { normalizeLowercaseStringOrEmpty } from "../../shared/string-coerce.js"; +import { + normalizeLowercaseStringOrEmpty, + normalizeOptionalString, +} from "../../shared/string-coerce.js"; import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../../utils/message-channel.js"; import { readStringParam } from "./common.js"; @@ -74,8 +77,7 @@ function validateGatewayUrlOverrideForAgentTools(params: { ]); let remoteKey: string | undefined; - const remoteUrl = - typeof cfg.gateway?.remote?.url === "string" ? cfg.gateway.remote.url.trim() : ""; + const remoteUrl = normalizeOptionalString(cfg.gateway?.remote?.url) ?? ""; if (remoteUrl) { try { const remote = canonicalizeToolGatewayWsUrl(remoteUrl); diff --git a/src/agents/tools/sessions-access.ts b/src/agents/tools/sessions-access.ts index f456f2efeea..d22579393ee 100644 --- a/src/agents/tools/sessions-access.ts +++ b/src/agents/tools/sessions-access.ts @@ -1,6 +1,9 @@ import type { OpenClawConfig } from "../../config/config.js"; import { isSubagentSessionKey, resolveAgentIdFromSessionKey } from "../../routing/session-key.js"; -import { normalizeLowercaseStringOrEmpty } from "../../shared/string-coerce.js"; +import { + normalizeLowercaseStringOrEmpty, + normalizeOptionalString, +} from "../../shared/string-coerce.js"; import { listSpawnedSessionKeys, resolveInternalSessionKey, @@ -64,14 +67,14 @@ export function resolveSandboxedSessionToolContext(params: { } { const { mainKey, alias } = resolveMainSessionAlias(params.cfg); const visibility = resolveSandboxSessionToolsVisibility(params.cfg); - const requesterInternalKey = - typeof params.agentSessionKey === "string" && params.agentSessionKey.trim() - ? resolveInternalSessionKey({ - key: params.agentSessionKey, - alias, - mainKey, - }) - : undefined; + const requesterSessionKey = normalizeOptionalString(params.agentSessionKey); + const requesterInternalKey = requesterSessionKey + ? resolveInternalSessionKey({ + key: requesterSessionKey, + alias, + mainKey, + }) + : undefined; const effectiveRequesterKey = requesterInternalKey ?? alias; const restrictToSpawned = params.sandboxed === true && @@ -97,7 +100,9 @@ export function createAgentToAgentPolicy(cfg: OpenClawConfig): AgentToAgentPolic return true; } return allowPatterns.some((pattern) => { - const raw = String(pattern ?? "").trim(); + const raw = + normalizeOptionalString(typeof pattern === "string" ? pattern : String(pattern ?? "")) ?? + ""; if (!raw) { return false; } diff --git a/src/agents/tools/sessions-send-tool.ts b/src/agents/tools/sessions-send-tool.ts index 602d055b9ac..cc09c415b3a 100644 --- a/src/agents/tools/sessions-send-tool.ts +++ b/src/agents/tools/sessions-send-tool.ts @@ -156,7 +156,7 @@ export function createSessionsSendTool(opts?: { params: resolveParams, timeoutMs: 10_000, }); - resolvedKey = typeof resolved?.key === "string" ? resolved.key.trim() : ""; + resolvedKey = normalizeOptionalString(resolved?.key) ?? ""; } catch (err) { const msg = formatErrorMessage(err); if (restrictToSpawned) { diff --git a/src/agents/tools/sessions-spawn-tool.ts b/src/agents/tools/sessions-spawn-tool.ts index acfde709a35..fdd84cf7c15 100644 --- a/src/agents/tools/sessions-spawn-tool.ts +++ b/src/agents/tools/sessions-spawn-tool.ts @@ -155,7 +155,7 @@ export function createSessionsSpawnTool( ); } const task = readStringParam(params, "task", { required: true }); - const label = typeof params.label === "string" ? params.label.trim() : ""; + const label = readStringParam(params, "label") ?? ""; const runtime = params.runtime === "acp" ? "acp" : "subagent"; const requestedAgentId = readStringParam(params, "agentId"); const resumeSessionId = readStringParam(params, "resumeSessionId");