refactor: dedupe core trimmed readers

This commit is contained in:
Peter Steinberger
2026-04-08 01:04:11 +01:00
parent 3174c6919d
commit 08cee3316d
6 changed files with 106 additions and 83 deletions

View File

@@ -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<string | undefined> {
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(
() =>

View File

@@ -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,

View File

@@ -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<string>();
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;
}

View File

@@ -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<Configure
await text({
message: "Auth profile id",
validate: (value) => {
const trimmed = String(value ?? "").trim();
const trimmed = normalizeStringifiedOptionalString(value) ?? "";
if (!trimmed) {
return "Required";
}
@@ -312,13 +319,13 @@ async function promptNewAuthProfileCandidate(agentId: string): Promise<Configure
const provider = assertNoCancel(
await text({
message: "Provider id",
validate: (value) => (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<string> }): 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<string> }): Pr
}),
"Secrets configure cancelled.",
);
return String(alias).trim();
return normalizeStringifiedOptionalString(alias) ?? "";
}
async function promptProviderSource(initial?: SecretRefSource): Promise<SecretRefSource> {
@@ -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({

View File

@@ -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<string[
if (!Array.isArray(extensions)) {
return [];
}
return extensions.map((entry) => (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<st
// --------------------------------------------------------------------------
function normalizeDockerLabelValue(raw: string | undefined): string | null {
const trimmed = raw?.trim() ?? "";
const trimmed = normalizeOptionalString(raw) ?? "";
if (!trimmed || trimmed === "<no value>") {
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) {

View File

@@ -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<string> {
@@ -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