diff --git a/src/agents/cli-auth-epoch.ts b/src/agents/cli-auth-epoch.ts index ac50b6227aa..23170739ea4 100644 --- a/src/agents/cli-auth-epoch.ts +++ b/src/agents/cli-auth-epoch.ts @@ -1,4 +1,5 @@ import crypto from "node:crypto"; +import { normalizeOptionalString } from "../shared/string-coerce.js"; import { loadAuthProfileStoreForRuntime } from "./auth-profiles/store.js"; import type { AuthProfileCredential, AuthProfileStore } from "./auth-profiles/types.js"; import { @@ -137,7 +138,7 @@ export async function resolveCliAuthEpoch(params: { authProfileId?: string; }): Promise { const provider = params.provider.trim(); - const authProfileId = params.authProfileId?.trim() || undefined; + const authProfileId = normalizeOptionalString(params.authProfileId); const parts: string[] = []; const localFingerprint = getLocalCliCredentialFingerprint(provider); diff --git a/src/agents/pi-embedded-runner/run.ts b/src/agents/pi-embedded-runner/run.ts index 39a9dc790f1..5588aa91213 100644 --- a/src/agents/pi-embedded-runner/run.ts +++ b/src/agents/pi-embedded-runner/run.ts @@ -10,6 +10,7 @@ import { sleepWithAbort } from "../../infra/backoff.js"; import { formatErrorMessage } from "../../infra/errors.js"; import { getGlobalHookRunner } from "../../plugins/hook-runner-global.js"; import { enqueueCommandInLane } from "../../process/command-queue.js"; +import { normalizeOptionalString } from "../../shared/string-coerce.js"; import { sanitizeForLog } from "../../terminal/ansi.js"; import { isMarkdownCapableMessageChannel } from "../../utils/message-channel.js"; import { resolveOpenClawAgentDir } from "../agent-paths.js"; @@ -118,7 +119,7 @@ function backfillSessionKey(params: { sessionKey?: string; agentId?: string; }): string | undefined { - const trimmed = params.sessionKey?.trim() || undefined; + const trimmed = normalizeOptionalString(params.sessionKey); if (trimmed) { return trimmed; } @@ -126,7 +127,7 @@ function backfillSessionKey(params: { return undefined; } try { - const resolved = params.agentId?.trim() + const resolved = normalizeOptionalString(params.agentId) ? resolveStoredSessionKeyForSessionId({ cfg: params.config, sessionId: params.sessionId, @@ -136,7 +137,7 @@ function backfillSessionKey(params: { cfg: params.config, sessionId: params.sessionId, }); - return resolved.sessionKey?.trim() || undefined; + return normalizeOptionalString(resolved.sessionKey); } catch (err) { log.warn( `[backfillSessionKey] Failed to resolve sessionKey for sessionId=${redactRunIdentifier(sanitizeForLog(params.sessionId))}: ${formatErrorMessage(err)}`, diff --git a/src/agents/pi-embedded-runner/runs.ts b/src/agents/pi-embedded-runner/runs.ts index c6b47484046..0a266285e8b 100644 --- a/src/agents/pi-embedded-runner/runs.ts +++ b/src/agents/pi-embedded-runner/runs.ts @@ -15,6 +15,7 @@ import { logSessionStateChange, } from "../../logging/diagnostic.js"; import { resolveGlobalSingleton } from "../../shared/global-singleton.js"; +import { normalizeOptionalString } from "../../shared/string-coerce.js"; export type EmbeddedPiQueueHandle = { kind?: "embedded"; @@ -57,7 +58,8 @@ const embeddedRunState = resolveGlobalSingleton(EMBEDDED_RUN_STATE_KEY, () => ({ modelSwitchRequests: new Map(), })); const ACTIVE_EMBEDDED_RUNS = - embeddedRunState.activeRuns ?? (embeddedRunState.activeRuns = new Map()); + embeddedRunState.activeRuns ?? + (embeddedRunState.activeRuns = new Map()); const ACTIVE_EMBEDDED_RUN_SNAPSHOTS = embeddedRunState.snapshots ?? (embeddedRunState.snapshots = new Map()); @@ -65,7 +67,8 @@ const ACTIVE_EMBEDDED_RUN_SESSION_IDS_BY_KEY = embeddedRunState.sessionIdsByKey ?? (embeddedRunState.sessionIdsByKey = new Map()); const EMBEDDED_RUN_WAITERS = - embeddedRunState.waiters ?? (embeddedRunState.waiters = new Map>()); + embeddedRunState.waiters ?? + (embeddedRunState.waiters = new Map>()); const EMBEDDED_RUN_MODEL_SWITCH_REQUESTS = embeddedRunState.modelSwitchRequests ?? (embeddedRunState.modelSwitchRequests = new Map()); @@ -242,8 +245,10 @@ export function requestEmbeddedRunModelSwitch( EMBEDDED_RUN_MODEL_SWITCH_REQUESTS.set(normalizedSessionId, { provider, model, - authProfileId: request.authProfileId?.trim() || undefined, - authProfileIdSource: request.authProfileId?.trim() ? request.authProfileIdSource : undefined, + authProfileId: normalizeOptionalString(request.authProfileId), + authProfileIdSource: normalizeOptionalString(request.authProfileId) + ? request.authProfileIdSource + : undefined, }); diag.debug( `model switch requested: sessionId=${normalizedSessionId} provider=${provider} model=${model}`, diff --git a/src/agents/sandbox/browser.ts b/src/agents/sandbox/browser.ts index 2bc9a1abdfe..3ecb2b8e7de 100644 --- a/src/agents/sandbox/browser.ts +++ b/src/agents/sandbox/browser.ts @@ -12,6 +12,7 @@ import { type ResolvedBrowserConfig, } from "../../plugin-sdk/browser-profiles.js"; import { defaultRuntime } from "../../runtime.js"; +import { normalizeOptionalString } from "../../shared/string-coerce.js"; import { BROWSER_BRIDGES } from "./browser-bridges.js"; import { computeSandboxBrowserConfigHash } from "./config-hash.js"; import { resolveSandboxBrowserDockerCreateConfig } from "./config.js"; @@ -295,8 +296,8 @@ export async function ensureSandboxBrowser(params: { ? resolveProfile(existing.bridge.state.resolved, DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME) : null; - let desiredAuthToken = params.bridgeAuth?.token?.trim() || undefined; - let desiredAuthPassword = params.bridgeAuth?.password?.trim() || undefined; + let desiredAuthToken = normalizeOptionalString(params.bridgeAuth?.token); + let desiredAuthPassword = normalizeOptionalString(params.bridgeAuth?.password); if (!desiredAuthToken && !desiredAuthPassword) { // Always require auth for the sandbox bridge server, even if gateway auth // mode doesn't produce a shared secret (e.g. trusted-proxy). diff --git a/src/agents/sandbox/novnc-auth.ts b/src/agents/sandbox/novnc-auth.ts index ee46617a840..5de849135a3 100644 --- a/src/agents/sandbox/novnc-auth.ts +++ b/src/agents/sandbox/novnc-auth.ts @@ -1,4 +1,5 @@ import crypto from "node:crypto"; +import { normalizeOptionalString } from "../../shared/string-coerce.js"; export const NOVNC_PASSWORD_ENV_KEY = "OPENCLAW_BROWSER_NOVNC_PASSWORD"; // pragma: allowlist secret const NOVNC_TOKEN_TTL_MS = 60 * 1000; @@ -65,7 +66,7 @@ export function issueNoVncObserverToken(params: { const token = crypto.randomBytes(24).toString("hex"); NO_VNC_OBSERVER_TOKENS.set(token, { noVncPort: params.noVncPort, - password: params.password?.trim() || undefined, + password: normalizeOptionalString(params.password), expiresAt: now + Math.max(1, params.ttlMs ?? NOVNC_TOKEN_TTL_MS), }); return token; diff --git a/src/cli/daemon-cli/lifecycle.ts b/src/cli/daemon-cli/lifecycle.ts index 30b319bcc92..1921fde4d25 100644 --- a/src/cli/daemon-cli/lifecycle.ts +++ b/src/cli/daemon-cli/lifecycle.ts @@ -8,6 +8,7 @@ import { signalVerifiedGatewayPidSync, } from "../../infra/gateway-processes.js"; import { defaultRuntime } from "../../runtime.js"; +import { normalizeOptionalString } from "../../shared/string-coerce.js"; import { theme } from "../../terminal/theme.js"; import { formatCliCommand } from "../command-format.js"; import { recoverInstalledLaunchAgent } from "./launchd-recovery.js"; @@ -77,8 +78,8 @@ async function assertUnmanagedGatewayRestartEnabled(port: number): Promise const probe = await probeGateway({ url: `${scheme}://127.0.0.1:${port}`, auth: { - token: process.env.OPENCLAW_GATEWAY_TOKEN?.trim() || undefined, - password: process.env.OPENCLAW_GATEWAY_PASSWORD?.trim() || undefined, + token: normalizeOptionalString(process.env.OPENCLAW_GATEWAY_TOKEN), + password: normalizeOptionalString(process.env.OPENCLAW_GATEWAY_PASSWORD), }, timeoutMs: 1_000, }).catch(() => null); diff --git a/src/gateway/client.ts b/src/gateway/client.ts index f54386eaa5a..563e2e06cc8 100644 --- a/src/gateway/client.ts +++ b/src/gateway/client.ts @@ -14,6 +14,7 @@ import { import { normalizeFingerprint } from "../infra/tls/fingerprint.js"; import { rawDataToString } from "../infra/ws.js"; import { logDebug, logError } from "../logger.js"; +import { normalizeOptionalString } from "../shared/string-coerce.js"; import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES, @@ -526,7 +527,7 @@ export class GatewayClient { err instanceof GatewayClientRequestError ? readConnectErrorDetailCode(err.details) : null; const shouldRetryWithDeviceToken = this.shouldRetryWithStoredDeviceToken({ error: err, - explicitGatewayToken: this.opts.token?.trim() || undefined, + explicitGatewayToken: normalizeOptionalString(this.opts.token), resolvedDeviceToken, storedToken: storedToken ?? undefined, }); @@ -661,10 +662,10 @@ export class GatewayClient { } private selectConnectAuth(role: string): SelectedConnectAuth { - const explicitGatewayToken = this.opts.token?.trim() || undefined; - const explicitBootstrapToken = this.opts.bootstrapToken?.trim() || undefined; - const explicitDeviceToken = this.opts.deviceToken?.trim() || undefined; - const authPassword = this.opts.password?.trim() || undefined; + const explicitGatewayToken = normalizeOptionalString(this.opts.token); + const explicitBootstrapToken = normalizeOptionalString(this.opts.bootstrapToken); + const explicitDeviceToken = normalizeOptionalString(this.opts.deviceToken); + const authPassword = normalizeOptionalString(this.opts.password); const storedAuth = this.loadStoredDeviceAuth(role); const storedToken = storedAuth?.token ?? null; const storedScopes = storedAuth?.scopes; diff --git a/src/gateway/probe-auth.ts b/src/gateway/probe-auth.ts index 4c245d0bee8..738c8fcef29 100644 --- a/src/gateway/probe-auth.ts +++ b/src/gateway/probe-auth.ts @@ -1,4 +1,5 @@ import type { OpenClawConfig } from "../config/config.js"; +import { normalizeOptionalString } from "../shared/string-coerce.js"; import { type ExplicitGatewayAuth, isGatewaySecretRefUnavailableError, @@ -28,8 +29,8 @@ function resolveExplicitProbeAuth(explicitAuth?: ExplicitGatewayAuth): { token?: string; password?: string; } { - const token = explicitAuth?.token?.trim() || undefined; - const password = explicitAuth?.password?.trim() || undefined; + const token = normalizeOptionalString(explicitAuth?.token); + const password = normalizeOptionalString(explicitAuth?.password); return { token, password }; } diff --git a/src/infra/exec-approval-reply.ts b/src/infra/exec-approval-reply.ts index c74cd8f52a0..4c94176511a 100644 --- a/src/infra/exec-approval-reply.ts +++ b/src/infra/exec-approval-reply.ts @@ -1,5 +1,6 @@ import type { ReplyPayload } from "../auto-reply/types.js"; import type { InteractiveReply, InteractiveReplyButton } from "../interactive/payload.js"; +import { normalizeOptionalString } from "../shared/string-coerce.js"; import { describeNativeExecApprovalClientSetup, listNativeExecApprovalClientLabels, @@ -348,9 +349,9 @@ export function buildExecApprovalPendingReplyPayload( approvalId: params.approvalId, approvalSlug: params.approvalSlug, approvalKind: "exec", - agentId: params.agentId?.trim() || undefined, + agentId: normalizeOptionalString(params.agentId), allowedDecisions, - sessionKey: params.sessionKey?.trim() || undefined, + sessionKey: normalizeOptionalString(params.sessionKey), }, }, }; diff --git a/src/infra/heartbeat-runner.ts b/src/infra/heartbeat-runner.ts index 74fa8f551c8..3980dea1ac3 100644 --- a/src/infra/heartbeat-runner.ts +++ b/src/infra/heartbeat-runner.ts @@ -49,6 +49,7 @@ import { toAgentStoreSessionKey, } from "../routing/session-key.js"; import { defaultRuntime, type RuntimeEnv } from "../runtime.js"; +import { normalizeOptionalString } from "../shared/string-coerce.js"; import { escapeRegExp } from "../utils.js"; import { formatErrorMessage, hasErrnoCode } from "./errors.js"; import { isWithinActiveHours } from "./heartbeat-active-hours.js"; @@ -1191,7 +1192,7 @@ export function startHeartbeatRunner(opts: { const reason = params?.reason; const requestedAgentId = params?.agentId ? normalizeAgentId(params.agentId) : undefined; - const requestedSessionKey = params?.sessionKey?.trim() || undefined; + const requestedSessionKey = normalizeOptionalString(params?.sessionKey); const isInterval = reason === "interval"; const startedAt = Date.now(); const now = startedAt; diff --git a/src/pairing/setup-code.ts b/src/pairing/setup-code.ts index 9a1ecb84963..3de919691a1 100644 --- a/src/pairing/setup-code.ts +++ b/src/pairing/setup-code.ts @@ -19,6 +19,7 @@ import { isRfc1918Ipv4Address, parseCanonicalIpAddress, } from "../shared/net/ip.js"; +import { normalizeOptionalString } from "../shared/string-coerce.js"; import { resolveTailnetHostWithRunner } from "../shared/tailscale-status.js"; export type PairingSetupPayload = { @@ -214,11 +215,11 @@ function pickTailnetIPv4( } function resolveGatewayTokenFromEnv(env: NodeJS.ProcessEnv): string | undefined { - return env.OPENCLAW_GATEWAY_TOKEN?.trim() || undefined; + return normalizeOptionalString(env.OPENCLAW_GATEWAY_TOKEN); } function resolveGatewayPasswordFromEnv(env: NodeJS.ProcessEnv): string | undefined { - return env.OPENCLAW_GATEWAY_PASSWORD?.trim() || undefined; + return normalizeOptionalString(env.OPENCLAW_GATEWAY_PASSWORD); } function resolvePairingSetupAuthLabel( diff --git a/src/plugin-sdk/approval-renderers.ts b/src/plugin-sdk/approval-renderers.ts index 8feebb43c8d..5fe6eedb15e 100644 --- a/src/plugin-sdk/approval-renderers.ts +++ b/src/plugin-sdk/approval-renderers.ts @@ -9,6 +9,7 @@ import { type PluginApprovalRequest, type PluginApprovalResolved, } from "../infra/plugin-approvals.js"; +import { normalizeOptionalString } from "../shared/string-coerce.js"; const DEFAULT_ALLOWED_DECISIONS = ["allow-once", "allow-always", "deny"] as const; @@ -34,9 +35,9 @@ export function buildApprovalPendingReplyPayload(params: { approvalId: params.approvalId, approvalSlug: params.approvalSlug, approvalKind: params.approvalKind ?? "exec", - agentId: params.agentId?.trim() || undefined, + agentId: normalizeOptionalString(params.agentId), allowedDecisions, - sessionKey: params.sessionKey?.trim() || undefined, + sessionKey: normalizeOptionalString(params.sessionKey), state: "pending", }, ...params.channelData,