From 08cee3316d06ee0d2f58357ed2977caee54c7404 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 8 Apr 2026 01:04:11 +0100 Subject: [PATCH] refactor: dedupe core trimmed readers --- src/infra/clawhub.ts | 22 ++++++------ src/node-host/invoke-system-run-plan.ts | 40 +++++++++++---------- src/pairing/pairing-store.ts | 23 ++++++------ src/secrets/configure.ts | 47 ++++++++++++++----------- src/security/audit-extra.async.ts | 27 +++++++++----- src/security/audit-extra.sync.ts | 30 +++++++++------- 6 files changed, 106 insertions(+), 83 deletions(-) diff --git a/src/infra/clawhub.ts b/src/infra/clawhub.ts index 447af93e786..b48962ed3f3 100644 --- a/src/infra/clawhub.ts +++ b/src/infra/clawhub.ts @@ -209,10 +209,10 @@ export class ClawHubRequestError extends Error { function normalizeBaseUrl(baseUrl?: string): string { const envValue = - process.env.OPENCLAW_CLAWHUB_URL?.trim() || - process.env.CLAWHUB_URL?.trim() || + normalizeOptionalString(process.env.OPENCLAW_CLAWHUB_URL) || + normalizeOptionalString(process.env.CLAWHUB_URL) || DEFAULT_CLAWHUB_URL; - const value = (baseUrl?.trim() || envValue).replace(/\/+$/, ""); + const value = (normalizeOptionalString(baseUrl) || envValue).replace(/\/+$/, ""); return value || DEFAULT_CLAWHUB_URL; } @@ -235,14 +235,14 @@ function extractTokenFromClawHubConfig(value: unknown): string | undefined { function resolveClawHubConfigPaths(): string[] { const explicit = - process.env.OPENCLAW_CLAWHUB_CONFIG_PATH?.trim() || - process.env.CLAWHUB_CONFIG_PATH?.trim() || - process.env.CLAWDHUB_CONFIG_PATH?.trim(); // legacy misspelling from older clawhub CLI builds; keep for back-compat + normalizeOptionalString(process.env.OPENCLAW_CLAWHUB_CONFIG_PATH) || + normalizeOptionalString(process.env.CLAWHUB_CONFIG_PATH) || + normalizeOptionalString(process.env.CLAWDHUB_CONFIG_PATH); // legacy misspelling from older clawhub CLI builds; keep for back-compat if (explicit) { return [explicit]; } - const xdgConfigHome = process.env.XDG_CONFIG_HOME?.trim(); + const xdgConfigHome = normalizeOptionalString(process.env.XDG_CONFIG_HOME); const configHome = xdgConfigHome && xdgConfigHome.length > 0 ? xdgConfigHome : path.join(os.homedir(), ".config"); const xdgPath = path.join(configHome, "clawhub", "config.json"); @@ -259,9 +259,9 @@ function resolveClawHubConfigPaths(): string[] { export async function resolveClawHubAuthToken(): Promise { const envToken = - process.env.OPENCLAW_CLAWHUB_TOKEN?.trim() || - process.env.CLAWHUB_TOKEN?.trim() || - process.env.CLAWHUB_AUTH_TOKEN?.trim(); + normalizeOptionalString(process.env.OPENCLAW_CLAWHUB_TOKEN) || + normalizeOptionalString(process.env.CLAWHUB_TOKEN) || + normalizeOptionalString(process.env.CLAWHUB_AUTH_TOKEN); if (envToken) { return envToken; } @@ -366,7 +366,7 @@ async function clawhubRequest( params: ClawHubRequestParams, ): Promise<{ response: Response; url: URL }> { const url = buildUrl(params); - const token = params.token?.trim() || (await resolveClawHubAuthToken()); + const token = normalizeOptionalString(params.token) || (await resolveClawHubAuthToken()); const controller = new AbortController(); const timeout = setTimeout( () => diff --git a/src/node-host/invoke-system-run-plan.ts b/src/node-host/invoke-system-run-plan.ts index abc2ff33f93..541eb2b5a18 100644 --- a/src/node-host/invoke-system-run-plan.ts +++ b/src/node-host/invoke-system-run-plan.ts @@ -145,6 +145,10 @@ function normalizeOptionFlag(token: string): string { return normalizeLowercaseStringOrEmpty(token.split("=", 1)[0]); } +function readTrimmedArgToken(argv: readonly string[], index: number): string { + return normalizeNullableString(argv[index]) ?? ""; +} + const POSIX_SHELL_OPTIONS_WITH_VALUE = new Set([ "--init-file", "--rcfile", @@ -333,7 +337,7 @@ function normalizePackageManagerExecToken(token: string): string { function unwrapPnpmExecInvocation(argv: string[]): string[] | null { let idx = 1; while (idx < argv.length) { - const token = argv[idx]?.trim() ?? ""; + const token = readTrimmedArgToken(argv, idx); if (!token) { idx += 1; continue; @@ -377,7 +381,7 @@ function unwrapPnpmExecInvocation(argv: string[]): string[] | null { function unwrapPnpmDlxInvocation(argv: string[]): string[] | null { let idx = 0; while (idx < argv.length) { - const token = argv[idx]?.trim() ?? ""; + const token = readTrimmedArgToken(argv, idx); if (!token) { idx += 1; continue; @@ -411,7 +415,7 @@ function unwrapPnpmDlxInvocation(argv: string[]): string[] | null { function unwrapDirectPackageExecInvocation(argv: string[]): string[] | null { let idx = 1; while (idx < argv.length) { - const token = argv[idx]?.trim() ?? ""; + const token = readTrimmedArgToken(argv, idx); if (!token) { idx += 1; continue; @@ -439,7 +443,7 @@ function unwrapDirectPackageExecInvocation(argv: string[]): string[] | null { function unwrapNpmExecInvocation(argv: string[]): string[] | null { let idx = 1; while (idx < argv.length) { - const token = argv[idx]?.trim() ?? ""; + const token = readTrimmedArgToken(argv, idx); if (!token) { idx += 1; continue; @@ -480,7 +484,7 @@ function resolvePosixShellScriptOperandIndex(argv: string[]): number | null { } let afterDoubleDash = false; for (let i = 1; i < argv.length; i += 1) { - const token = argv[i]?.trim() ?? ""; + const token = readTrimmedArgToken(argv, i); if (!token) { continue; } @@ -517,7 +521,7 @@ function resolveOptionFilteredFileOperandIndex(params: { }): number | null { let afterDoubleDash = false; for (let i = params.startIndex; i < params.argv.length; i += 1) { - const token = params.argv[i]?.trim() ?? ""; + const token = readTrimmedArgToken(params.argv, i); if (!token) { continue; } @@ -549,7 +553,7 @@ function resolveOptionFilteredPositionalIndex(params: { }): number | null { let afterDoubleDash = false; for (let i = params.startIndex; i < params.argv.length; i += 1) { - const token = params.argv[i]?.trim() ?? ""; + const token = readTrimmedArgToken(params.argv, i); if (!token) { continue; } @@ -583,7 +587,7 @@ function collectExistingFileOperandIndexes(params: { let afterDoubleDash = false; const hits: number[] = []; for (let i = params.startIndex; i < params.argv.length; i += 1) { - const token = params.argv[i]?.trim() ?? ""; + const token = readTrimmedArgToken(params.argv, i); if (!token) { continue; } @@ -607,7 +611,7 @@ function collectExistingFileOperandIndexes(params: { hits.push(i); return { hits, sawOptionValueFile: true }; } - const nextToken = params.argv[i + 1]?.trim() ?? ""; + const nextToken = readTrimmedArgToken(params.argv, i + 1); if (!inlineValue && nextToken && resolvesToExistingFileSync(nextToken, params.cwd)) { hits.push(i + 1); return { hits, sawOptionValueFile: true }; @@ -651,7 +655,7 @@ function resolveBunScriptOperandIndex(params: { if (directIndex === null) { return null; } - const directToken = params.argv[directIndex]?.trim() ?? ""; + const directToken = readTrimmedArgToken(params.argv, directIndex); if (directToken === "run") { return resolveOptionFilteredFileOperandIndex({ argv: params.argv, @@ -673,7 +677,7 @@ function resolveDenoRunScriptOperandIndex(params: { argv: string[]; cwd: string | undefined; }): number | null { - if ((params.argv[1]?.trim() ?? "") !== "run") { + if (readTrimmedArgToken(params.argv, 1) !== "run") { return null; } return resolveOptionFilteredFileOperandIndex({ @@ -687,7 +691,7 @@ function resolveDenoRunScriptOperandIndex(params: { function hasRubyUnsafeApprovalFlag(argv: string[]): boolean { let afterDoubleDash = false; for (let i = 1; i < argv.length; i += 1) { - const token = argv[i]?.trim() ?? ""; + const token = readTrimmedArgToken(argv, i); if (!token) { continue; } @@ -714,7 +718,7 @@ function hasRubyUnsafeApprovalFlag(argv: string[]): boolean { function hasPerlUnsafeApprovalFlag(argv: string[]): boolean { let afterDoubleDash = false; for (let i = 1; i < argv.length; i += 1) { - const token = argv[i]?.trim() ?? ""; + const token = readTrimmedArgToken(argv, i); if (!token) { continue; } @@ -753,7 +757,7 @@ function resolveMutableFileOperandIndex(argv: string[], cwd: string | undefined) return shellIndex === null ? null : unwrapped.baseIndex + shellIndex; } if (MUTABLE_ARGV1_INTERPRETER_PATTERNS.some((pattern) => pattern.test(executable))) { - const operand = unwrapped.argv[1]?.trim() ?? ""; + const operand = readTrimmedArgToken(unwrapped.argv, 1); if (operand && operand !== "-" && !operand.startsWith("-")) { return unwrapped.baseIndex + 1; } @@ -810,7 +814,7 @@ function shellPayloadNeedsStableBinding(shellCommand: string, cwd: string | unde if (snapshot.snapshot) { return true; } - const firstToken = argv[0]?.trim() ?? ""; + const firstToken = readTrimmedArgToken(argv, 0); return resolvesToExistingFileSync(firstToken, cwd); } @@ -843,7 +847,7 @@ function pnpmDlxInvocationNeedsFailClosedBinding(argv: string[], cwd: string | u let idx = 1; while (idx < argv.length) { - const token = argv[idx]?.trim() ?? ""; + const token = readTrimmedArgToken(argv, idx); if (!token) { idx += 1; continue; @@ -876,7 +880,7 @@ function pnpmDlxInvocationNeedsFailClosedBinding(argv: string[], cwd: string | u function pnpmDlxTailNeedsFailClosedBinding(argv: string[], cwd: string | undefined): boolean { let idx = 0; while (idx < argv.length) { - const token = argv[idx]?.trim() ?? ""; + const token = readTrimmedArgToken(argv, idx); if (!token) { idx += 1; continue; @@ -935,7 +939,7 @@ export function resolveMutableFileOperandSnapshotSync(params: { } return { ok: true, snapshot: null }; } - const rawOperand = params.argv[argvIndex]?.trim(); + const rawOperand = readTrimmedArgToken(params.argv, argvIndex); if (!rawOperand) { return { ok: false, diff --git a/src/pairing/pairing-store.ts b/src/pairing/pairing-store.ts index e17e703e344..1f30a797ae8 100644 --- a/src/pairing/pairing-store.ts +++ b/src/pairing/pairing-store.ts @@ -11,7 +11,9 @@ import { readJsonFileWithFallback, writeJsonFileAtomically } from "../plugin-sdk import { DEFAULT_ACCOUNT_ID } from "../routing/session-key.js"; import { normalizeLowercaseStringOrEmpty, + normalizeNullableString, normalizeOptionalString, + normalizeStringifiedOptionalString, } from "../shared/string-coerce.js"; const PAIRING_CODE_LENGTH = 8; @@ -98,7 +100,7 @@ function resolveAllowFromPath( accountId?: string, ): string { const base = safeChannelKey(channel); - const normalizedAccountId = typeof accountId === "string" ? accountId.trim() : ""; + const normalizedAccountId = normalizeOptionalString(accountId) ?? ""; if (!normalizedAccountId) { return path.join(resolveCredentialsDir(env), `${base}-allowFrom.json`); } @@ -279,7 +281,7 @@ function resolveAllowFromAccountId(accountId?: string): string { } function normalizeId(value: string | number): string { - return String(value).trim(); + return normalizeStringifiedOptionalString(value) ?? ""; } function normalizeAllowEntry(channel: PairingChannel, entry: string): string { @@ -292,7 +294,7 @@ function normalizeAllowEntry(channel: PairingChannel, entry: string): string { } const adapter = getPairingAdapter(channel); const normalized = adapter?.normalizeAllowEntry ? adapter.normalizeAllowEntry(trimmed) : trimmed; - return String(normalized).trim(); + return normalizeOptionalString(normalized) ?? ""; } function normalizeAllowFromList(channel: PairingChannel, store: AllowFromStore): string[] { @@ -310,7 +312,7 @@ function dedupePreserveOrder(entries: string[]): string[] { const seen = new Set(); const out: string[] = []; for (const entry of entries) { - const normalized = String(entry).trim(); + const normalized = normalizeOptionalString(entry) ?? ""; if (!normalized || seen.has(normalized)) { continue; } @@ -749,7 +751,7 @@ export async function upsertChannelPairingRequest(params: { params.meta && typeof params.meta === "object" ? Object.fromEntries( Object.entries(params.meta) - .map(([k, v]) => [k, String(v ?? "").trim()] as const) + .map(([k, v]) => [k, normalizeOptionalString(v) ?? ""] as const) .filter(([_, v]) => Boolean(v)), ) : undefined; @@ -769,17 +771,12 @@ export async function upsertChannelPairingRequest(params: { return requestMatchesAccountId(r, normalizedMatchingAccountId); }); const existingCodes = new Set( - reqs.map((req) => - String(req.code ?? "") - .trim() - .toUpperCase(), - ), + reqs.map((req) => (normalizeOptionalString(req.code) ?? "").toUpperCase()), ); if (existingIdx >= 0) { const existing = reqs[existingIdx]; - const existingCode = - existing && typeof existing.code === "string" ? existing.code.trim() : ""; + const existingCode = normalizeOptionalString(existing?.code) ?? ""; const code = existingCode || generateUniqueCode(existingCodes); const next: PairingRequest = { id, @@ -838,7 +835,7 @@ export async function approveChannelPairingCode(params: { env?: NodeJS.ProcessEnv; }): Promise<{ id: string; entry?: PairingRequest } | null> { const env = params.env ?? process.env; - const code = params.code.trim().toUpperCase(); + const code = (normalizeNullableString(params.code) ?? "").toUpperCase(); if (!code) { return null; } diff --git a/src/secrets/configure.ts b/src/secrets/configure.ts index 59d6de3c562..2aa31153031 100644 --- a/src/secrets/configure.ts +++ b/src/secrets/configure.ts @@ -9,7 +9,11 @@ import type { OpenClawConfig } from "../config/config.js"; import type { SecretProviderConfig, SecretRef, SecretRefSource } from "../config/types.secrets.js"; import { isSafeExecutableValue } from "../infra/exec-safety.js"; import { normalizeAgentId } from "../routing/session-key.js"; -import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js"; +import { + normalizeOptionalLowercaseString, + normalizeOptionalString, + normalizeStringifiedOptionalString, +} from "../shared/string-coerce.js"; import { runSecretsApply, type SecretsApplyResult } from "./apply.js"; import { createSecretsConfigIO } from "./config-io.js"; import { @@ -198,7 +202,7 @@ async function promptOptionalPositiveInt(params: { message: params.message, initialValue: params.initialValue === undefined ? "" : String(params.initialValue), validate: (value) => { - const trimmed = String(value ?? "").trim(); + const trimmed = normalizeStringifiedOptionalString(value) ?? ""; if (!trimmed) { return undefined; } @@ -211,7 +215,10 @@ async function promptOptionalPositiveInt(params: { }), "Secrets configure cancelled.", ); - const parsed = parseOptionalPositiveInt(String(raw ?? ""), params.max); + const parsed = parseOptionalPositiveInt( + normalizeStringifiedOptionalString(raw) ?? "", + params.max, + ); return parsed; } @@ -221,7 +228,7 @@ function configureCandidateKey(candidate: { agentId?: string; }): string { if (candidate.configFile === "auth-profiles.json") { - return `auth-profiles:${String(candidate.agentId ?? "").trim()}:${candidate.path}`; + return `auth-profiles:${normalizeOptionalString(candidate.agentId) ?? ""}:${candidate.path}`; } return `openclaw:${candidate.path}`; } @@ -285,7 +292,7 @@ async function promptNewAuthProfileCandidate(agentId: string): Promise { - const trimmed = String(value ?? "").trim(); + const trimmed = normalizeStringifiedOptionalString(value) ?? ""; if (!trimmed) { return "Required"; } @@ -312,13 +319,13 @@ async function promptNewAuthProfileCandidate(agentId: string): Promise (String(value ?? "").trim().length > 0 ? undefined : "Required"), + validate: (value) => (normalizeStringifiedOptionalString(value) ? undefined : "Required"), }), "Secrets configure cancelled.", ); - const profileIdTrimmed = String(profileId).trim(); - const providerTrimmed = String(provider).trim(); + const profileIdTrimmed = normalizeStringifiedOptionalString(profileId) ?? ""; + const providerTrimmed = normalizeStringifiedOptionalString(provider) ?? ""; if (credentialType === "token") { return { type: "auth-profiles.token.token", @@ -349,7 +356,7 @@ async function promptProviderAlias(params: { existingAliases: Set }): Pr message: "Provider alias", initialValue: "default", validate: (value) => { - const trimmed = String(value ?? "").trim(); + const trimmed = normalizeStringifiedOptionalString(value) ?? ""; if (!trimmed) { return "Required"; } @@ -364,7 +371,7 @@ async function promptProviderAlias(params: { existingAliases: Set }): Pr }), "Secrets configure cancelled.", ); - return String(alias).trim(); + return normalizeStringifiedOptionalString(alias) ?? ""; } async function promptProviderSource(initial?: SecretRefSource): Promise { @@ -404,7 +411,7 @@ async function promptFileProvider( message: "File path (absolute)", initialValue: base?.path ?? "", validate: (value) => { - const trimmed = String(value ?? "").trim(); + const trimmed = normalizeStringifiedOptionalString(value) ?? ""; if (!trimmed) { return "Required"; } @@ -442,7 +449,7 @@ async function promptFileProvider( return { source: "file", - path: String(filePath).trim(), + path: normalizeStringifiedOptionalString(filePath) ?? "", mode, ...(timeoutMs ? { timeoutMs } : {}), ...(maxBytes ? { maxBytes } : {}), @@ -469,7 +476,7 @@ async function promptExecProvider( message: "Command path (absolute)", initialValue: base?.command ?? "", validate: (value) => { - const trimmed = String(value ?? "").trim(); + const trimmed = normalizeStringifiedOptionalString(value) ?? ""; if (!trimmed) { return "Required"; } @@ -490,7 +497,7 @@ async function promptExecProvider( message: "Args JSON array (blank for none)", initialValue: JSON.stringify(base?.args ?? []), validate: (value) => { - const trimmed = String(value ?? "").trim(); + const trimmed = normalizeStringifiedOptionalString(value) ?? ""; if (!trimmed) { return undefined; } @@ -571,12 +578,12 @@ async function promptExecProvider( "Secrets configure cancelled.", ); - const args = await parseArgsInput(String(argsRaw ?? "")); + const args = await parseArgsInput(normalizeStringifiedOptionalString(argsRaw) ?? ""); const trustedDirs = parseCsv(String(trustedDirsRaw ?? "")); return { source: "exec", - command: String(command).trim(), + command: normalizeStringifiedOptionalString(command) ?? "", ...(args && args.length > 0 ? { args } : {}), ...(timeoutMs ? { timeoutMs } : {}), ...(noOutputTimeoutMs ? { noOutputTimeoutMs } : {}), @@ -864,7 +871,7 @@ export async function runSecretsConfigureInteractive( message: "Provider alias", initialValue: providerInitialValue, validate: (value) => { - const trimmed = String(value ?? "").trim(); + const trimmed = normalizeStringifiedOptionalString(value) ?? ""; if (!trimmed) { return "Required"; } @@ -876,7 +883,7 @@ export async function runSecretsConfigureInteractive( }), "Secrets configure cancelled.", ); - const providerAlias = String(provider).trim(); + const providerAlias = normalizeStringifiedOptionalString(provider) ?? ""; const suggestedIdFromExistingRef = existingRef?.source === source ? existingRef.id : undefined; let suggestedId = suggestedIdFromExistingRef; @@ -894,7 +901,7 @@ export async function runSecretsConfigureInteractive( message: "Secret id", initialValue: suggestedId, validate: (value) => { - const trimmed = String(value ?? "").trim(); + const trimmed = normalizeStringifiedOptionalString(value) ?? ""; if (!trimmed) { return "Required"; } @@ -909,7 +916,7 @@ export async function runSecretsConfigureInteractive( const ref: SecretRef = { source, provider: providerAlias, - id: String(id).trim(), + id: normalizeStringifiedOptionalString(id) ?? "", }; if (ref.source === "exec" && !allowExecInPreflight) { const staticError = getSkippedExecRefStaticError({ diff --git a/src/security/audit-extra.async.ts b/src/security/audit-extra.async.ts index 6ec4ed74ed8..6cd41d9622f 100644 --- a/src/security/audit-extra.async.ts +++ b/src/security/audit-extra.async.ts @@ -27,7 +27,10 @@ import type { AgentToolsConfig } from "../config/types.tools.js"; import { readInstalledPackageVersion } from "../infra/package-update-utils.js"; import { normalizePluginsConfig } from "../plugins/config-state.js"; import { normalizeAgentId } from "../routing/session-key.js"; -import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js"; +import { + normalizeOptionalLowercaseString, + normalizeOptionalString, +} from "../shared/string-coerce.js"; import { formatPermissionDetail, formatPermissionRemediation, @@ -77,7 +80,7 @@ function expandTilde(p: string, env: NodeJS.ProcessEnv): string | null { if (!p.startsWith("~")) { return p; } - const home = typeof env.HOME === "string" && env.HOME.trim() ? env.HOME.trim() : null; + const home = normalizeOptionalString(env.HOME) ?? null; if (!home) { return null; } @@ -104,7 +107,7 @@ async function readPluginManifestExtensions(pluginPath: string): Promise (typeof entry === "string" ? entry.trim() : "")).filter(Boolean); + return extensions.map((entry) => normalizeOptionalString(entry) ?? "").filter(Boolean); } function formatCodeSafetyDetails(findings: SkillScanFinding[], rootDir: string): string { @@ -434,7 +437,7 @@ async function listWorkspaceSkillMarkdownFiles(workspaceDir: string): Promise") { return null; } @@ -490,8 +493,10 @@ async function readSandboxBrowserHashLabels(params: { } function parsePublishedHostFromDockerPortLine(line: string): string | null { - const trimmed = line.trim(); - const rhs = trimmed.includes("->") ? (trimmed.split("->").at(-1)?.trim() ?? "") : trimmed; + const trimmed = normalizeOptionalString(line) ?? ""; + const rhs = trimmed.includes("->") + ? (normalizeOptionalString(trimmed.split("->").at(-1)) ?? "") + : trimmed; if (!rhs) { return null; } @@ -1061,7 +1066,12 @@ export async function collectStateDeepFilesystemFindings(params: { const agentIds = Array.isArray(params.cfg.agents?.list) ? params.cfg.agents?.list - .map((a) => (a && typeof a === "object" && typeof a.id === "string" ? a.id.trim() : "")) + .map( + (a) => + normalizeOptionalString( + a && typeof a === "object" ? (a as { id?: unknown }).id : undefined, + ) ?? "", + ) .filter(Boolean) : []; const defaultAgentId = resolveDefaultAgentId(params.cfg); @@ -1132,8 +1142,7 @@ export async function collectStateDeepFilesystemFindings(params: { } } - const logFile = - typeof params.cfg.logging?.file === "string" ? params.cfg.logging.file.trim() : ""; + const logFile = normalizeOptionalString(params.cfg.logging?.file) ?? ""; if (logFile) { const expanded = logFile.startsWith("~") ? expandTilde(logFile, params.env) : logFile; if (expanded) { diff --git a/src/security/audit-extra.sync.ts b/src/security/audit-extra.sync.ts index 78a2333cfbe..fa7ec82ffff 100644 --- a/src/security/audit-extra.sync.ts +++ b/src/security/audit-extra.sync.ts @@ -27,7 +27,11 @@ import { DEFAULT_DANGEROUS_NODE_COMMANDS, resolveNodeCommandAllowlist, } from "../gateway/node-command-policy.js"; -import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js"; +import { + normalizeOptionalLowercaseString, + normalizeOptionalString, + normalizeStringifiedOptionalString, +} from "../shared/string-coerce.js"; import { pickSandboxToolPolicy } from "./audit-tool-policy.js"; export type SecurityAuditFinding = { @@ -161,7 +165,11 @@ function hasConfiguredDockerConfig( } function normalizeNodeCommand(value: unknown): string { - return typeof value === "string" ? value.trim() : ""; + return normalizeOptionalString(value) ?? ""; +} + +function isWildcardEntry(value: unknown): boolean { + return normalizeStringifiedOptionalString(value) === "*"; } function listKnownNodeCommands(cfg: OpenClawConfig): Set { @@ -350,12 +358,12 @@ function listPotentialMultiUserSignals(cfg: OpenClawConfig): string[] { } const allowFrom = Array.isArray(section.allowFrom) ? section.allowFrom : []; - if (allowFrom.some((entry) => String(entry).trim() === "*")) { + if (allowFrom.some((entry) => isWildcardEntry(entry))) { out.add(`${basePath}.allowFrom includes "*"`); } const groupAllowFrom = Array.isArray(section.groupAllowFrom) ? section.groupAllowFrom : []; - if (groupAllowFrom.some((entry) => String(entry).trim() === "*")) { + if (groupAllowFrom.some((entry) => isWildcardEntry(entry))) { out.add(`${basePath}.groupAllowFrom includes "*"`); } @@ -367,7 +375,7 @@ function listPotentialMultiUserSignals(cfg: OpenClawConfig): string[] { out.add(`${basePath}.dm.policy="open"`); } const dmAllowFrom = Array.isArray(dmSection.allowFrom) ? dmSection.allowFrom : []; - if (dmAllowFrom.some((entry) => String(entry).trim() === "*")) { + if (dmAllowFrom.some((entry) => isWildcardEntry(entry))) { out.add(`${basePath}.dm.allowFrom includes "*"`); } } @@ -475,8 +483,7 @@ export function collectSyncedFolderFindings(params: { export function collectSecretsInConfigFindings(cfg: OpenClawConfig): SecurityAuditFinding[] { const findings: SecurityAuditFinding[] = []; - const password = - typeof cfg.gateway?.auth?.password === "string" ? cfg.gateway.auth.password.trim() : ""; + const password = normalizeOptionalString(cfg.gateway?.auth?.password) ?? ""; if (password && !looksLikeEnvRef(password)) { findings.push({ checkId: "config.secrets.gateway_password_in_config", @@ -489,7 +496,7 @@ export function collectSecretsInConfigFindings(cfg: OpenClawConfig): SecurityAud }); } - const hooksToken = typeof cfg.hooks?.token === "string" ? cfg.hooks.token.trim() : ""; + const hooksToken = normalizeOptionalString(cfg.hooks?.token) ?? ""; if (cfg.hooks?.enabled === true && hooksToken && !looksLikeEnvRef(hooksToken)) { findings.push({ checkId: "config.secrets.hooks_token_in_config", @@ -512,7 +519,7 @@ export function collectHooksHardeningFindings( return findings; } - const token = typeof cfg.hooks?.token === "string" ? cfg.hooks.token.trim() : ""; + const token = normalizeOptionalString(cfg.hooks?.token) ?? ""; if (token && token.length < 24) { findings.push({ checkId: "hooks.token_too_short", @@ -550,7 +557,7 @@ export function collectHooksHardeningFindings( }); } - const rawPath = typeof cfg.hooks?.path === "string" ? cfg.hooks.path.trim() : ""; + const rawPath = normalizeOptionalString(cfg.hooks?.path) ?? ""; if (rawPath === "/") { findings.push({ checkId: "hooks.path_root", @@ -562,8 +569,7 @@ export function collectHooksHardeningFindings( } const allowRequestSessionKey = cfg.hooks?.allowRequestSessionKey === true; - const defaultSessionKey = - typeof cfg.hooks?.defaultSessionKey === "string" ? cfg.hooks.defaultSessionKey.trim() : ""; + const defaultSessionKey = normalizeOptionalString(cfg.hooks?.defaultSessionKey) ?? ""; const allowedAgentIds = resolveAllowedAgentIds(cfg.hooks?.allowedAgentIds); const allowedPrefixes = Array.isArray(cfg.hooks?.allowedSessionKeyPrefixes) ? cfg.hooks.allowedSessionKeyPrefixes