refactor: consolidate coercion ownership (#122299)

* refactor: consolidate coercion ownership

Centralize four canonical coercion helpers, migrate exact core and plugin duplicates through narrow Plugin SDK facades, and enforce declaration and plugin-normalization ownership boundaries.

The sweep adds eight focused SDK exports while deleting more production and tooling code than it adds. User-visible behavior is unchanged except for safer equivalent object and UI parsing at existing boundaries.

* fix: guard integer option ownership

Register resolveIntegerOption with the canonical function owner and extend the declaration-guard fixture so future local duplicates fail validation.

* fix: keep integer helpers on numeric facade

Remove the unshipped duplicate string-coerce exports and route every affected plugin consumer through the existing number-runtime contract.

* fix: point numeric coercion to number runtime

Make boundary and declaration diagnostics recommend the canonical numeric facade, with failing-before coverage for both guidance paths.
This commit is contained in:
Peter Steinberger
2026-08-11 17:14:53 -07:00
committed by GitHub
parent 88dba70ec5
commit 964c8c84c1
258 changed files with 1774 additions and 1910 deletions
@@ -1 +1 @@
{"contentHash":"ca54298c1a0adf9550f58fc13881d7e7f37e4c755a5f917a595bc4f0177d9e20","entrypoint":"agent-harness-runtime","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime"}
{"contentHash":"b017f177c844e59f8c9771773fd9fe13bd49777097859ecec3e90057d6cc3709","entrypoint":"agent-harness-runtime","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime"}
+1 -1
View File
@@ -1 +1 @@
{"contentHash":"00743220201e85945df77cc8e276a4842f93602c971e06459ea00dd029f063ff","entrypoint":"agent-harness","importSpecifier":"openclaw/plugin-sdk/agent-harness"}
{"contentHash":"9695a1112df503dd92e36a5502aa3930289d6828a562917ea5b8d6b7c25a0755","entrypoint":"agent-harness","importSpecifier":"openclaw/plugin-sdk/agent-harness"}
+1 -1
View File
@@ -1 +1 @@
{"contentHash":"b0d043c2fc4d36023fa7bd6472c9b9cc5f0ee17ff4375d598e59fecb45cbe85c","entrypoint":"channel-core","importSpecifier":"openclaw/plugin-sdk/channel-core"}
{"contentHash":"96449168bf95c5c22ff75ba7d50d21cb2d9c73666c5ceef95450d76273cd36ca","entrypoint":"channel-core","importSpecifier":"openclaw/plugin-sdk/channel-core"}
@@ -1 +1 @@
{"contentHash":"555310d087ce5a7a801f6291ee837283eb182e9bf86f294e11dc0d4559c9c268","entrypoint":"channel-entry-contract","importSpecifier":"openclaw/plugin-sdk/channel-entry-contract"}
{"contentHash":"f1e6d92d125cdaca72317819b5ba7619cbfcb67ee72752bb009af78a8f753b38","entrypoint":"channel-entry-contract","importSpecifier":"openclaw/plugin-sdk/channel-entry-contract"}
@@ -1 +1 @@
{"contentHash":"08111748e2944e6bf1571eda856a46a00c813e2d90fd3da04a9c473b033bc38c","entrypoint":"channel-message","importSpecifier":"openclaw/plugin-sdk/channel-message"}
{"contentHash":"1b3770f22431dbbce8ffcbffd4496a95d2afaac82172aa48b73c9c8bceac7ca5","entrypoint":"channel-message","importSpecifier":"openclaw/plugin-sdk/channel-message"}
@@ -1 +1 @@
{"contentHash":"4561d105811249b490dcc691888e082fbb5365b0652fee8e97d272c55f7c1eba","entrypoint":"channel-outbound","importSpecifier":"openclaw/plugin-sdk/channel-outbound"}
{"contentHash":"fdea77b1259c9799177f9c2b327a6d759058d0df0ef339834b6c0a6a9d04b130","entrypoint":"channel-outbound","importSpecifier":"openclaw/plugin-sdk/channel-outbound"}
@@ -1 +1 @@
{"contentHash":"8bef6c7c70abc4745029421fef29125646effa27bb8947541e0ca9bea4e2da9b","entrypoint":"channel-plugin-common","importSpecifier":"openclaw/plugin-sdk/channel-plugin-common"}
{"contentHash":"ddc4dd3f139c5858912bad00560051731ef386eaec43957343b01f76224ecca3","entrypoint":"channel-plugin-common","importSpecifier":"openclaw/plugin-sdk/channel-plugin-common"}
+1 -1
View File
@@ -1 +1 @@
{"contentHash":"140e69b3a97558ca94edd1fc83fc528e33565d0b3b7b16a2fb7dbf9bd9deb0be","entrypoint":"core","importSpecifier":"openclaw/plugin-sdk/core"}
{"contentHash":"8f806aa43ddd9298176cacb5971b8ddac7f2c1d25101deaa92b3c9e1f788e09a","entrypoint":"core","importSpecifier":"openclaw/plugin-sdk/core"}
+1 -1
View File
@@ -1 +1 @@
{"contentHash":"af02ae51d5c0941991928b998ecac5584639e50912532082fa0d16fe0a40e43f","entrypoint":"discord","importSpecifier":"openclaw/plugin-sdk/discord"}
{"contentHash":"11138710259d030a81c55a9e506254428328931945b6c6b79fc969e832d56901","entrypoint":"discord","importSpecifier":"openclaw/plugin-sdk/discord"}
+1 -1
View File
@@ -1 +1 @@
{"contentHash":"e0523b4936d7c7ec043265de9e2a8f6289d32dacba0a28c3999ba3a0975dfc21","entrypoint":"error-runtime","importSpecifier":"openclaw/plugin-sdk/error-runtime"}
{"contentHash":"1f61b7d845d0172c62a5413cd96559796dbe338b38866f91782ff74485aef96e","entrypoint":"error-runtime","importSpecifier":"openclaw/plugin-sdk/error-runtime"}
@@ -1 +1 @@
{"contentHash":"de31ab324cc57e8f525cac7559813e83a434c3cb114124388558c0b35e094adc","entrypoint":"inbound-reply-dispatch","importSpecifier":"openclaw/plugin-sdk/inbound-reply-dispatch"}
{"contentHash":"180d24e66df9e1242f1ba78f4aea182c79340280d022db048b227071892fbb59","entrypoint":"inbound-reply-dispatch","importSpecifier":"openclaw/plugin-sdk/inbound-reply-dispatch"}
@@ -1 +1 @@
{"contentHash":"f559df4c86dd30b151db7e159944df99f5870a8564b4ef07adfbff4826737aa4","entrypoint":"meeting-runtime","importSpecifier":"openclaw/plugin-sdk/meeting-runtime"}
{"contentHash":"5a99888e906478be43c13f67f99fb2f74c7396eb834202e7d872f0e2ee60f144","entrypoint":"meeting-runtime","importSpecifier":"openclaw/plugin-sdk/meeting-runtime"}
+1 -1
View File
@@ -1 +1 @@
{"contentHash":"9f9e1c49a46f04964615be21556834f4d7d6042aedef30812973efbc2af9871c","entrypoint":"plugin-entry","importSpecifier":"openclaw/plugin-sdk/plugin-entry"}
{"contentHash":"1896a2342b27ee5f055bf77f9a826f76ddc6520c9dce44789d6d612cfcdd17dc","entrypoint":"plugin-entry","importSpecifier":"openclaw/plugin-sdk/plugin-entry"}
+1 -1
View File
@@ -1 +1 @@
{"contentHash":"b1883cb0c123488b0d8f93934cc27dca32e349d17bab1119e0d273655b368c4a","entrypoint":"plugin-runtime","importSpecifier":"openclaw/plugin-sdk/plugin-runtime"}
{"contentHash":"fcdd433db990480373e6caea357c69cbbc754bcbf492fe3371cd21491d9bd04f","entrypoint":"plugin-runtime","importSpecifier":"openclaw/plugin-sdk/plugin-runtime"}
@@ -1 +1 @@
{"contentHash":"7923737351da1ca1c0c0b3c3ce3d105a61739f633d27da4cc8b92c957ccf397f","entrypoint":"provider-catalog-runtime","importSpecifier":"openclaw/plugin-sdk/provider-catalog-runtime"}
{"contentHash":"9459eb7103be5db943a12ebbf69b62f9a06b60cf5a24df9a8b72fa2bd4242dce","entrypoint":"provider-catalog-runtime","importSpecifier":"openclaw/plugin-sdk/provider-catalog-runtime"}
@@ -1 +1 @@
{"contentHash":"14d56ffc88afba3e8c83ace0894f0187860a45b739d562b96e290d2235c45294","entrypoint":"string-coerce-runtime","importSpecifier":"openclaw/plugin-sdk/string-coerce-runtime"}
{"contentHash":"347f68c5497a100307cffbb43c2f88e653bed7b7451066b7eb19266dd3c16660","entrypoint":"string-coerce-runtime","importSpecifier":"openclaw/plugin-sdk/string-coerce-runtime"}
+1 -1
View File
@@ -1 +1 @@
{"contentHash":"b2281fac79519417397a7670e25392c7e7e592a84e3ed00459b947b6bef2955b","entrypoint":"tool-plugin","importSpecifier":"openclaw/plugin-sdk/tool-plugin"}
{"contentHash":"1d4374297f908a74d78873c746fc0624240182a96c28a064a47f91f6260049bb","entrypoint":"tool-plugin","importSpecifier":"openclaw/plugin-sdk/tool-plugin"}
@@ -1 +1 @@
{"contentHash":"14ae7f9d841c6c4e2702239ed2aeb126f89039df8434bf12773a495486a04ff4","entrypoint":"webhook-ingress","importSpecifier":"openclaw/plugin-sdk/webhook-ingress"}
{"contentHash":"f5bf0f3889cd0e4a5b1de4e110098400573d46c508c962c386db3334246bc9fe","entrypoint":"webhook-ingress","importSpecifier":"openclaw/plugin-sdk/webhook-ingress"}
+1 -4
View File
@@ -8,6 +8,7 @@ import { createRequire } from "node:module";
import os from "node:os";
import path from "node:path";
import { readJsonFileWithFallback } from "openclaw/plugin-sdk/json-store";
import { isRecord as isConfigRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
import {
parse as parseToml,
stringify as stringifyToml,
@@ -802,10 +803,6 @@ function extractConfiguredAdapterArgs(params: {
return undefined;
}
function isConfigRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}
function mergeConfigRecords(
base: Record<string, unknown>,
override: Record<string, unknown>,
+5 -5
View File
@@ -1,6 +1,9 @@
import path from "node:path";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
import { parseStrictPositiveInteger } from "openclaw/plugin-sdk/number-runtime";
import {
parseStrictPositiveInteger,
resolveIntegerOption,
} from "openclaw/plugin-sdk/number-runtime";
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry";
import { isPathInside } from "openclaw/plugin-sdk/security-runtime";
import {
@@ -52,10 +55,7 @@ function parseOptionalPositiveInt(value: unknown, fallback: number): number {
}
function clampInt(value: number | undefined, fallback: number, min: number, max: number): number {
if (!Number.isFinite(value)) {
return fallback;
}
return Math.max(min, Math.min(max, Math.floor(value as number)));
return resolveIntegerOption(value, fallback, { min, max });
}
function normalizeTranscriptDir(value: unknown): string {
+4 -9
View File
@@ -7,7 +7,10 @@ import { join } from "node:path";
import type { GoogleAuthOptions } from "google-auth-library";
import { resolveProviderEndpoint } from "openclaw/plugin-sdk/provider-http";
import { tryReadSecretFileSync } from "openclaw/plugin-sdk/secret-file-runtime";
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
import {
normalizeLowercaseStringOrEmpty,
normalizeOptionalString as normalizeOptionalSecretInput,
} from "openclaw/plugin-sdk/string-coerce-runtime";
const ANTHROPIC_VERTEX_DEFAULT_REGION = "global";
const ANTHROPIC_VERTEX_REGION_RE = /^[a-z0-9-]+$/;
@@ -19,14 +22,6 @@ type AnthropicVertexAdcCredentials = NonNullable<GoogleAuthOptions["credentials"
quota_project_id?: unknown;
};
function normalizeOptionalSecretInput(value: unknown): string | undefined {
if (typeof value !== "string") {
return undefined;
}
const trimmed = value.trim();
return trimmed || undefined;
}
/** Resolve the configured Vertex region, defaulting to global. */
export function resolveAnthropicVertexRegion(env: NodeJS.ProcessEnv = process.env): string {
const region =
+2 -8
View File
@@ -144,12 +144,6 @@ function readPerTokenPrice(value: unknown): number | undefined {
: undefined;
}
function readStringArray(value: unknown): string[] {
return Array.isArray(value)
? value.filter((item): item is string => typeof item === "string")
: [];
}
function applyLiveReasoningEffortCompat(
fallbackCompat: ModelCompatConfig,
supportsReasoningEffort: boolean,
@@ -177,7 +171,7 @@ function projectLiveModel(
}
const hasLiveFeatures = Array.isArray(row.supported_features);
const features = new Set(readStringArray(row.supported_features));
const features = new Set(filterStringEntries(row.supported_features));
const pricing = asNonArrayRecord(row.pricing);
const inputPrice = readPerTokenPrice(pricing.prompt);
const outputPrice = readPerTokenPrice(pricing.completion);
@@ -256,4 +250,4 @@ export function resolveBasetenDynamicModel(modelId: string) {
compat: buildBasetenModelCompat(id),
};
}
import { asNonArrayRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
import { asNonArrayRecord, filterStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime";
@@ -1,3 +1,4 @@
import { resolveOptionalIntegerOption } from "openclaw/plugin-sdk/number-runtime";
/**
* Runtime dependency barrel for the Browser agent tool.
*
@@ -9,11 +10,14 @@ import { getRuntimeConfig } from "./sdk-config.js";
export { getRuntimeConfig };
/** Resolve global image downscaling for screenshots returned to agent tools. */
export function resolveRuntimeImageSanitization(): { maxDimensionPx: number } | undefined {
const configured = getRuntimeConfig().agents?.defaults?.imageMaxDimensionPx;
if (typeof configured !== "number" || !Number.isFinite(configured)) {
const maxDimensionPx = resolveOptionalIntegerOption(
getRuntimeConfig().agents?.defaults?.imageMaxDimensionPx,
{ min: 1 },
);
if (maxDimensionPx === undefined) {
return undefined;
}
return { maxDimensionPx: Math.max(1, Math.floor(configured)) };
return { maxDimensionPx };
}
export {
callGatewayTool,
@@ -1,11 +1,11 @@
/**
* Chrome DevTools Protocol URL, fetch, and socket helpers.
*
* Handles CDP URL normalization, SSRF-guarded HTTP discovery, credential
* redaction/headers, and request/response correlation over WebSocket.
*/
import { createHash } from "node:crypto";
import { parseBrowserHttpUrl, redactCdpUrl } from "openclaw/plugin-sdk/browser-config";
import { toStringifiedError } from "openclaw/plugin-sdk/error-runtime";
import { readProviderJsonResponse } from "openclaw/plugin-sdk/provider-http";
import { sleepWithAbort } from "openclaw/plugin-sdk/runtime-env";
import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime";
@@ -35,8 +35,7 @@ import { normalizeBrowserTimerDelayMs } from "./timer-delay.js";
const CDP_URL_IN_TEXT_RE = /\b(?:https?|wss?):\/\/[^\s"'<>`]+/gi;
export { isLoopbackHost };
export { parseBrowserHttpUrl, redactCdpUrl };
export { isLoopbackHost, parseBrowserHttpUrl, redactCdpUrl };
/**
* Returns true when the URL uses a WebSocket protocol (ws: or wss:).
@@ -529,7 +528,7 @@ function createCdpSender(ws: WebSocket, opts?: { commandTimeoutMs?: number }) {
} catch (err) {
pending.delete(id);
clearPendingTimer(entry);
reject(err instanceof Error ? err : new Error(String(err)));
reject(toStringifiedError(err));
}
});
};
@@ -549,7 +548,7 @@ function createCdpSender(ws: WebSocket, opts?: { commandTimeoutMs?: number }) {
// non-Error branch would require synthetically emitting on the socket,
// which the library treats as an unhandled error and hangs the test.
/* c8 ignore next */
closeWithError(err instanceof Error ? err : new Error(String(err)));
closeWithError(toStringifiedError(err));
});
ws.on("message", (data) => {
@@ -798,7 +797,7 @@ export async function withCdpSocket<T>(
// Error from Node's `ws` library, the latter is already an Error. The
// non-Error wrap is defensive and structurally unreachable.
/* c8 ignore next */
closeWithError(err instanceof Error ? err : new Error(String(err)));
closeWithError(toStringifiedError(err));
// Cancellation on the final attempt must not become a handshake error.
opts?.signal?.throwIfAborted();
if (attempt >= maxHandshakeRetries || !shouldRetryCdpHandshakeError(err)) {
@@ -821,7 +820,7 @@ export async function withCdpSocket<T>(
try {
return await fn(send);
} catch (err) {
closeWithError(err instanceof Error ? err : new Error(String(err)));
closeWithError(toStringifiedError(err));
throw err;
} finally {
ws.close();
@@ -12,11 +12,6 @@ import {
type NormalizedChromeMcpProfileOptions,
} from "./chrome-mcp-contracts.js";
function normalizeChromeMcpUserDataDir(userDataDir?: string): string | undefined {
const trimmed = userDataDir?.trim();
return trimmed ? trimmed : undefined;
}
function normalizeChromeMcpStringList(values?: string[]): string[] {
return Array.isArray(values)
? values.filter(
@@ -35,7 +30,7 @@ export function normalizeChromeMcpOptions(
const command = normalizeOptionalString(options.mcpCommand) ?? DEFAULT_CHROME_MCP_COMMAND;
return {
command,
userDataDir: normalizeChromeMcpUserDataDir(options.userDataDir),
userDataDir: normalizeOptionalString(options.userDataDir),
browserUrl: normalizeOptionalString(options.cdpUrl),
extraArgs: normalizeChromeMcpStringList(options.mcpArgs),
};
@@ -1,4 +1,10 @@
import { asNullableRecord, isRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
import {
asNullableRecord,
asFiniteNumber,
filterStringEntries,
isRecord,
normalizeOptionalString,
} from "openclaw/plugin-sdk/string-coerce-runtime";
/**
* Managed Chrome graphics diagnostics.
*
@@ -24,11 +30,11 @@ type ChromeGraphicsProbeOptions = {
};
function readChromeString(value: unknown): string {
return typeof value === "string" ? value.trim() : "";
return normalizeOptionalString(value) ?? "";
}
function readChromeNumber(value: unknown): number {
return typeof value === "number" && Number.isFinite(value) ? value : 0;
return asFiniteNumber(value) ?? 0;
}
function readStringRecord(value: unknown): Record<string, string> {
@@ -42,12 +48,6 @@ function readStringRecord(value: unknown): Record<string, string> {
return Object.fromEntries(entries);
}
function readStringArray(value: unknown): string[] {
return Array.isArray(value)
? value.filter((item): item is string => typeof item === "string")
: [];
}
function readSize(value: unknown): { width: number; height: number } {
const size = asNullableRecord(value);
return {
@@ -162,7 +162,7 @@ function normalizeChromeGraphicsInfo(
devices,
featureStatus,
disabledFeatures,
driverBugWorkarounds: readStringArray(gpu.driverBugWorkarounds),
driverBugWorkarounds: filterStringEntries(gpu.driverBugWorkarounds),
videoDecoding: readVideoDecoding(gpu.videoDecoding),
videoEncoding: readVideoEncoding(gpu.videoEncoding),
};
+1 -6
View File
@@ -4,6 +4,7 @@ import type {
ProviderToolSchemaDiagnostic,
} from "openclaw/plugin-sdk/plugin-entry";
import { findUnsupportedSchemaKeywords } from "openclaw/plugin-sdk/provider-tools";
import { asOptionalRecord as readRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
const PERPLEXITY_UNSUPPORTED_SCHEMA_KEYWORDS = new Set([
"patternProperties",
@@ -36,12 +37,6 @@ const SCHEMA_VALUE_KEYS = new Set([
"contentSchema",
]);
function readRecord(value: unknown): Record<string, unknown> | undefined {
return value && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: undefined;
}
// JSON Schema allows `type` to be an array; a union containing "object" still
// admits objects, so it needs `properties` for Perplexity too.
function isObjectType(type: unknown): boolean {
+2 -2
View File
@@ -6,6 +6,7 @@ import type { ChannelGatewayContext } from "openclaw/plugin-sdk/channel-contract
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
import { channelReadyPatch, channelStoppedPatch } from "openclaw/plugin-sdk/gateway-runtime";
import { sleepWithAbort } from "openclaw/plugin-sdk/runtime-env";
import { readStringField } from "openclaw/plugin-sdk/string-coerce-runtime";
import { rawDataToString } from "openclaw/plugin-sdk/webhook-ingress";
import type { RawData } from "ws";
import { resolveClickClackInboundAccess } from "./access.js";
@@ -28,8 +29,7 @@ import type {
const CLICKCLACK_EVENT_PAGE_LIMIT = 500;
function payloadString(event: ClickClackEvent, key: string): string {
const value = event.payload?.[key];
return typeof value === "string" ? value : "";
return readStringField(event.payload, key) ?? "";
}
function eventCorrelationId(event: ClickClackEvent): string | undefined {
+1 -1
View File
@@ -24,7 +24,7 @@ const PROVIDER_ID = "cloudflare-ai-gateway";
const PROVIDER_ENV_VAR = "CLOUDFLARE_AI_GATEWAY_API_KEY";
const PROFILE_ID = "cloudflare-ai-gateway:default";
function readRequiredTextInput(value: unknown): string {
return typeof value === "string" ? value.trim() : "";
return normalizeOptionalString(value) ?? "";
}
async function resolveCloudflareGatewayMetadataInteractive(ctx: {
@@ -13,6 +13,7 @@ import {
type NativeHookRelayProcessResponse,
type NativeHookRelayRegistrationHandle,
} from "openclaw/plugin-sdk/agent-harness-runtime";
import { coerceErrorMessage as formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
import {
normalizeTrimmedStringList,
readStringField as readString,
@@ -1332,7 +1333,4 @@ function joinDescriptionLinesWithinLimit(lines: string[], maxLength: number): st
return description;
}
function formatErrorMessage(error: unknown): string {
return error instanceof Error ? error.message : String(error);
}
/* oxlint-disable max-lines -- TODO: split this grandfathered oversized file. */
@@ -3,6 +3,7 @@
* or whether OpenClaw must keep exec/process on a configured node host.
*/
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
import { normalizeAgentId } from "openclaw/plugin-sdk/routing";
import { resolveSandboxRuntimeStatus } from "openclaw/plugin-sdk/sandbox";
import { getSessionEntry, type SessionEntry } from "openclaw/plugin-sdk/session-store-runtime";
@@ -16,12 +17,6 @@ type ExecHostOverride = {
type AgentEntry = NonNullable<NonNullable<OpenClawConfig["agents"]>["list"]>[number];
const DEFAULT_AGENT_ID = "main";
const VALID_AGENT_ID_PATTERN = /^[a-z0-9][a-z0-9_-]{0,63}$/i;
const INVALID_AGENT_ID_CHARS_PATTERN = /[^a-z0-9_-]+/g;
const LEADING_DASH_PATTERN = /^-+/;
const TRAILING_DASH_PATTERN = /-+$/;
/** Effective execution-host policy for the Codex app-server native tool surface. */
export type CodexNativeExecutionPolicy = {
nativeToolSurfaceAllowed: boolean;
@@ -197,25 +192,7 @@ function resolveDefaultPolicyAgentId(agents: AgentEntry[]): string {
function normalizeAgentIdOrDefault(value?: string | null): string | undefined {
const normalized = normalizeAgentId(value);
return normalized === DEFAULT_AGENT_ID && !(value ?? "").trim() ? undefined : normalized;
}
function normalizeAgentId(value?: string | null): string {
const trimmed = (value ?? "").trim();
if (!trimmed) {
return DEFAULT_AGENT_ID;
}
const normalized = trimmed.toLowerCase();
if (VALID_AGENT_ID_PATTERN.test(trimmed)) {
return normalized;
}
return (
normalized
.replace(INVALID_AGENT_ID_CHARS_PATTERN, "-")
.replace(LEADING_DASH_PATTERN, "")
.replace(TRAILING_DASH_PATTERN, "")
.slice(0, 64) || DEFAULT_AGENT_ID
);
return normalized === "main" && !(value ?? "").trim() ? undefined : normalized;
}
function normalizeExecTarget(value?: string | null): ExecTarget | undefined {
+2 -24
View File
@@ -15,6 +15,7 @@ import type {
PluginHookInboundClaimEvent,
} from "openclaw/plugin-sdk/plugin-entry";
import type { ReplyPayload } from "openclaw/plugin-sdk/reply-payload";
import { normalizeAgentId } from "openclaw/plugin-sdk/routing";
import {
getSessionEntry,
resolveStorePath,
@@ -122,11 +123,6 @@ import { isIncognitoSessionKey } from "./incognito-session.js";
import { resumeCodexCliSessionOnNode } from "./node-cli-sessions.js";
const DEFAULT_BOUND_TURN_TIMEOUT_MS = 20 * 60_000;
const DEFAULT_AGENT_ID = "main";
const VALID_AGENT_ID_PATTERN = /^[a-z0-9][a-z0-9_-]{0,63}$/i;
const INVALID_AGENT_ID_CHARS_PATTERN = /[^a-z0-9_-]+/g;
const LEADING_DASH_PATTERN = /^-+/;
const TRAILING_DASH_PATTERN = /-+$/;
const NATIVE_CONVERSATION_INTERACTIVE_APPROVALS_UNAVAILABLE =
"OpenClaw native Codex conversation binding cannot route interactive approvals yet; use the Codex harness or explicit /acp spawn codex for that workflow.";
@@ -1484,25 +1480,7 @@ function resolveDefaultPolicyAgentId(config: ResolvedCodexConversationConfig): s
function normalizeAgentIdOrDefault(value?: string | null): string | undefined {
const normalized = normalizeAgentId(value);
return normalized === DEFAULT_AGENT_ID && !(value ?? "").trim() ? undefined : normalized;
}
function normalizeAgentId(value?: string | null): string {
const trimmed = (value ?? "").trim();
if (!trimmed) {
return DEFAULT_AGENT_ID;
}
const normalized = trimmed.toLowerCase();
if (VALID_AGENT_ID_PATTERN.test(trimmed)) {
return normalized;
}
return (
normalized
.replace(INVALID_AGENT_ID_CHARS_PATTERN, "-")
.replace(LEADING_DASH_PATTERN, "")
.replace(TRAILING_DASH_PATTERN, "")
.slice(0, 64) || DEFAULT_AGENT_ID
);
return normalized === "main" && !(value ?? "").trim() ? undefined : normalized;
}
function isCodexThreadNotFoundError(error: unknown): boolean {
+1 -4
View File
@@ -1,5 +1,6 @@
// Codex plugin module implements apply behavior.
import path from "node:path";
import { coerceErrorMessage as formatCodexMigrationError } from "openclaw/plugin-sdk/error-runtime";
import {
applyMigrationManualItem,
markMigrationItemConflict,
@@ -293,10 +294,6 @@ function isCodexPluginInventoryLoadError(error: unknown): boolean {
return message.includes("codex app-server plugin/list timed out");
}
function formatCodexMigrationError(error: unknown): string {
return error instanceof Error ? error.message : String(error);
}
function resolveTargetCodexAppServer(ctx: MigrationProviderContext) {
return resolveCodexAppServerRuntimeOptions({
pluginConfig: readCodexPluginConfig(ctx.config),
@@ -1,4 +1,4 @@
import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
import { asFiniteNumber, isRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
import { sanitizeTerminalText } from "openclaw/plugin-sdk/text-chunking";
import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
import type { CodexThread, CodexThreadTurnsListResponse } from "./app-server/protocol.js";
@@ -305,10 +305,6 @@ export function parseJsonParams(paramsJSON?: string | null): unknown {
}
}
function readFiniteNumber(value: unknown): number | undefined {
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
}
function parseOptionalCatalogString(
value: unknown,
field: string,
@@ -385,9 +381,9 @@ function parseCatalogSession(
const sessionKey = options.allowSessionKey
? parseOptionalCatalogString(value.sessionKey, "OpenClaw session key", MAX_SESSION_KEY_LENGTH)
: undefined;
const createdAt = readFiniteNumber(value.createdAt);
const updatedAt = readFiniteNumber(value.updatedAt);
const recencyAt = value.recencyAt === null ? null : readFiniteNumber(value.recencyAt);
const createdAt = asFiniteNumber(value.createdAt);
const updatedAt = asFiniteNumber(value.updatedAt);
const recencyAt = value.recencyAt === null ? null : asFiniteNumber(value.recencyAt);
return {
threadId: value.threadId,
status,
+1 -6
View File
@@ -124,10 +124,6 @@ if (process.env.VITEST === "true") {
});
}
function readConfigBoolean(config: ComfyProviderConfig, key: string): boolean | undefined {
return asBoolean(config[key]);
}
function readConfigInteger(config: ComfyProviderConfig, key: string): number | undefined {
const value = config[key];
return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : undefined;
@@ -710,8 +706,7 @@ export async function runComfyWorkflow(params: {
throw new Error("Comfy Cloud API key missing");
}
const explicitAllowPrivateNetwork =
readConfigBoolean(capabilityConfig, "allowPrivateNetwork") === true;
const explicitAllowPrivateNetwork = asBoolean(capabilityConfig.allowPrivateNetwork) === true;
const { baseUrl, allowPrivateNetwork, headers, dispatcherPolicy } =
resolveProviderHttpRequestConfig({
baseUrl: normalizeOptionalString(capabilityConfig.baseUrl),
+3 -6
View File
@@ -17,6 +17,7 @@ import {
type AgentHarnessResetParams,
} from "openclaw/plugin-sdk/agent-harness-runtime";
import type { PluginStateSyncKeyedStore } from "openclaw/plugin-sdk/plugin-state-runtime";
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
import type { CopilotSessionConfig } from "./src/attempt.js";
import { createCopilotByokAuth, resolveCopilotAuth, tokenFingerprint } from "./src/auth-bridge.js";
import { createCopilotByokProxy } from "./src/byok-proxy.js";
@@ -454,10 +455,10 @@ function computeSessionKey(
(typeof p.model === "string" ? p.model : "");
const requestTransport =
p.model && typeof p.model === "object" ? getModelProviderRequestTransport(p.model) : undefined;
const requestAuthMode = readSessionString(
const requestAuthMode = normalizeOptionalString(
requestTransport?.auth?.mode ?? modelObj.request?.auth?.mode,
);
const azureApiVersion = readSessionString(
const azureApiVersion = normalizeOptionalString(
modelObj.azureApiVersion ?? modelObj.params?.azureApiVersion,
);
// resolveCopilotAuth can throw when an explicit `auth.gitHubToken`
@@ -559,10 +560,6 @@ function computeSessionKey(
return parts.join("|");
}
function readSessionString(value: unknown): string | undefined {
return typeof value === "string" && value.trim() ? value.trim() : undefined;
}
function fingerprintSessionValue(value: unknown): string {
return typeof value === "string" && value ? tokenFingerprint(value) : "";
}
+3 -3
View File
@@ -1,5 +1,8 @@
import type { MessageOptions, SessionConfig, Tool as SdkTool } from "@github/copilot-sdk";
import type { AgentMessage, SandboxContext } from "openclaw/plugin-sdk/agent-harness-runtime";
import { toStringifiedError as toCopilotError } from "openclaw/plugin-sdk/error-runtime";
export { toCopilotError };
import {
detectAndLoadAgentHarnessPromptImages,
getModelProviderRequestTransport,
@@ -439,9 +442,6 @@ export function resolvePoolAcquire(params: AttemptParamsLike): {
provider,
};
}
export function toCopilotError(error: unknown): Error {
return error instanceof Error ? error : new Error(String(error));
}
export function isSdkSendAndWaitTimeoutError(error: unknown): boolean {
if (error === null || typeof error !== "object") {
return false;
+5 -14
View File
@@ -2,7 +2,10 @@
import type { ProviderConfig } from "@github/copilot-sdk";
import { isNonSecretApiKeyMarker } from "openclaw/plugin-sdk/provider-auth";
import { isBlockedHostnameOrIp } from "openclaw/plugin-sdk/ssrf-runtime";
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
import {
filterStringRecord,
normalizeOptionalString,
} from "openclaw/plugin-sdk/string-coerce-runtime";
import { tokenFingerprint } from "./auth-bridge.js";
const COPILOT_BYOK_PROVIDER_ERROR =
@@ -96,7 +99,7 @@ export function resolveCopilotProvider(params: {
const api = normalizeOptionalString(params.model.api)?.toLowerCase() ?? "openai-responses";
const provider = resolveProviderType(api, baseUrl, params.model.azureApiVersion);
const resolvedApiKey = resolveProviderCredential(params.resolvedApiKey);
const headers = resolveProviderHeaders(params.model.headers);
const headers = filterStringRecord(params.model.headers);
const requestAuthMode = normalizeOptionalString(params.model.requestAuthMode)?.toLowerCase();
const usePreparedRequestAuth =
requestAuthMode !== undefined && requestAuthMode !== "provider-default";
@@ -322,15 +325,3 @@ function resolveProviderCredential(value: string | undefined): string | undefine
const credential = normalizeOptionalString(value);
return credential && !isNonSecretApiKeyMarker(credential) ? credential : undefined;
}
function resolveProviderHeaders(
headers: Record<string, string | null | undefined> | undefined,
): Record<string, string> | undefined {
if (!headers) {
return undefined;
}
const resolved = Object.fromEntries(
Object.entries(headers).filter(([, value]) => typeof value === "string"),
) as Record<string, string>;
return Object.keys(resolved).length > 0 ? resolved : undefined;
}
+1 -7
View File
@@ -1,6 +1,7 @@
// Copilot plugin module implements runtime behavior.
import { normalize, resolve, sep } from "node:path";
import type { CopilotClient, CopilotClientOptions } from "@github/copilot-sdk";
import { toStringifiedError as toCopilotRuntimeError } from "openclaw/plugin-sdk/error-runtime";
import { loadCopilotSdk } from "./sdk-loader.js";
// SAFETY: The pool reuses CopilotClient instances per normalized PoolKey and does not
@@ -389,10 +390,3 @@ function normalizeCopilotHome(copilotHome: string): string {
}
return normalizedHome;
}
function toCopilotRuntimeError(error: unknown): Error {
if (error instanceof Error) {
return error;
}
return new Error(String(error));
}
+1 -4
View File
@@ -23,6 +23,7 @@ import {
sanitizeToolResult,
} from "openclaw/plugin-sdk/agent-harness-runtime";
import { createAgentHarnessToolSurfaceRuntime } from "openclaw/plugin-sdk/agent-harness-tool-runtime";
import { toStringifiedError as toCopilotToolError } from "openclaw/plugin-sdk/error-runtime";
type CreateOpenClawCodingTools =
(typeof import("openclaw/plugin-sdk/agent-harness"))["createOpenClawCodingTools"];
@@ -911,7 +912,3 @@ function findDuplicateToolNames(sourceTools: AnyAgentTool[]): string[] {
.map(([name]) => name)
.toSorted();
}
function toCopilotToolError(error: unknown): Error {
return error instanceof Error ? error : new Error(String(error));
}
@@ -8,7 +8,7 @@ import {
resolveBootstrapContextForRun,
resolveUserPath,
} from "openclaw/plugin-sdk/agent-harness-runtime";
import { hasNonEmptyString } from "openclaw/plugin-sdk/string-coerce-runtime";
import { readNonBlankString } from "openclaw/plugin-sdk/string-coerce-runtime";
// Filenames the Copilot SDK already loads natively from the working
// directory / instructionDirectories (per
@@ -88,10 +88,10 @@ export async function resolveCopilotWorkspaceBootstrapContext(params: {
const bootstrapContext = await resolveBootstrapContextForRun({
workspaceDir,
config: attempt.config,
sessionKey: readNonEmptyString((attempt as { sessionKey?: unknown }).sessionKey),
sessionId: readNonEmptyString(attempt.sessionId),
sessionKey: readNonBlankString((attempt as { sessionKey?: unknown }).sessionKey),
sessionId: readNonBlankString(attempt.sessionId),
chatType: attempt.chatType,
agentId: readNonEmptyString(attempt.agentId),
agentId: readNonBlankString(attempt.agentId),
warn: params.warn,
contextMode: attempt.bootstrapContextMode,
runKind: attempt.bootstrapContextRunKind,
@@ -235,12 +235,8 @@ function getCopilotContextFileBasename(filePath: string): string {
return normalizeCopilotContextFilePath(filePath).split("/").pop() ?? "";
}
function readNonEmptyString(value: unknown): string | undefined {
return hasNonEmptyString(value) ? value : undefined;
}
function readResolvedWorkspacePath(value: unknown): string | undefined {
const raw = readNonEmptyString(value);
const raw = readNonBlankString(value);
if (!raw) {
return undefined;
}
@@ -2,6 +2,9 @@ import { createHash } from "node:crypto";
import fs from "node:fs";
import path from "node:path";
import { WorkerProviderError, type WorkerProfile } from "openclaw/plugin-sdk/plugin-entry";
import { normalizeOptionalString as nonEmptyString } from "openclaw/plugin-sdk/string-coerce-runtime";
export { nonEmptyString };
const PROFILE_KEYS = new Set([
"binary",
@@ -39,14 +42,6 @@ type CrabboxProfile = {
type IsExecutable = (candidate: string) => boolean;
export function nonEmptyString(value: unknown): string | undefined {
if (typeof value !== "string") {
return undefined;
}
const trimmed = value.trim();
return trimmed || undefined;
}
function requirePositiveDuration(value: unknown, key: string): string {
const duration = nonEmptyString(value);
if (!duration || !isPositiveGoDuration(duration)) {
@@ -1,6 +1,7 @@
import { SpanKind } from "@opentelemetry/api";
import { GEN_AI_OPERATION_NAME_VALUE_INVOKE_AGENT } from "@opentelemetry/semantic-conventions/incubating";
import { normalizeDiagnosticValue } from "openclaw/plugin-sdk/diagnostic-runtime";
import { asFiniteNumber, asFiniteNumberInRange } from "openclaw/plugin-sdk/string-coerce-runtime";
import type { DiagnosticEventPayload } from "../api.js";
import { redactSensitiveText } from "../api.js";
import {
@@ -48,11 +49,11 @@ export function genAiOperationName(
}
export function positiveFiniteNumber(value: number | undefined): number | undefined {
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : undefined;
return asFiniteNumberInRange(value, { min: 0, minExclusive: true });
}
function nonNegativeFiniteNumber(value: number | undefined): number | undefined {
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
return asFiniteNumberInRange(value, { min: 0 });
}
export function assignPositiveNumberAttr(
@@ -88,8 +89,9 @@ function assignNumberAttr(
key: string,
value: number | undefined,
): void {
if (typeof value === "number" && Number.isFinite(value)) {
attrs[key] = value;
const normalized = asFiniteNumber(value);
if (normalized !== undefined) {
attrs[key] = normalized;
}
}
@@ -1,3 +1,4 @@
import { asBoolean } from "openclaw/plugin-sdk/string-coerce-runtime";
// Discord plugin module implements runtime.shared behavior.
import {
parseAvailableTags,
@@ -45,13 +46,6 @@ export function readDiscordAutoArchiveDurationParam(
return value;
}
function readDiscordBooleanParam(
params: Record<string, unknown>,
key: string,
): boolean | undefined {
return typeof params[key] === "boolean" ? params[key] : undefined;
}
export function createDiscordActionOptions<
T extends Record<string, unknown> = Record<string, never>,
>(params: {
@@ -80,7 +74,7 @@ export function readDiscordChannelCreateParams(
parentId: parentId ?? undefined,
topic: readStringParam(params, "topic") ?? undefined,
position: readNonNegativeIntegerParam(params, "position") ?? undefined,
nsfw: readDiscordBooleanParam(params, "nsfw"),
nsfw: asBoolean(params.nsfw),
};
}
@@ -92,10 +86,10 @@ export function readDiscordChannelEditParams(params: Record<string, unknown>): D
topic: readStringParam(params, "topic") ?? undefined,
position: readNonNegativeIntegerParam(params, "position") ?? undefined,
parentId: parentId === undefined ? undefined : parentId,
nsfw: readDiscordBooleanParam(params, "nsfw"),
nsfw: asBoolean(params.nsfw),
rateLimitPerUser: readNonNegativeIntegerParam(params, "rateLimitPerUser") ?? undefined,
archived: readDiscordBooleanParam(params, "archived"),
locked: readDiscordBooleanParam(params, "locked"),
archived: asBoolean(params.archived),
locked: asBoolean(params.locked),
autoArchiveDuration: readDiscordAutoArchiveDurationParam(params, "autoArchiveDuration"),
availableTags: parseAvailableTags(params.availableTags),
};
@@ -10,11 +10,6 @@ type ActiveDiscordTurnThreadRoute = {
const activeRoutes = new Map<string, Set<ActiveDiscordTurnThreadRoute>>();
function normalizeId(value: string | undefined): string | undefined {
const trimmed = value?.trim();
return trimmed || undefined;
}
export function beginDiscordActiveTurnThreadRoute(
sessionKey: string | undefined,
route: ActiveDiscordTurnThreadRoute,
@@ -96,3 +91,4 @@ function findDiscordActiveTurnThreadReplyRoute(params: {
(!route.accountId || !params.accountId || route.accountId === params.accountId),
);
}
import { normalizeOptionalString as normalizeId } from "openclaw/plugin-sdk/string-coerce-runtime";
+1 -5
View File
@@ -1,4 +1,5 @@
import type { DiscordAccountConfig } from "openclaw/plugin-sdk/config-contracts";
import { normalizeOptionalString as readNonEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
type DiscordActivitiesConfigResolution =
| {
@@ -11,11 +12,6 @@ type DiscordActivitiesConfigResolution =
reason: "not-configured" | "missing-client-secret";
};
function readNonEmpty(value: string | undefined): string | undefined {
const trimmed = value?.trim();
return trimmed || undefined;
}
export function resolveDiscordActivitiesConfig(
account: DiscordAccountConfig,
env: NodeJS.ProcessEnv = process.env,
+1 -10
View File
@@ -3,7 +3,7 @@ import { inspect } from "node:util";
import { gunzipSync } from "node:zlib";
import {
clampTimerTimeoutMs,
parseFiniteNumber,
resolveIntegerOption as normalizeIntegerOption,
resolveTimerTimeoutMs,
} from "openclaw/plugin-sdk/number-runtime";
import { readResponseWithLimit } from "openclaw/plugin-sdk/response-limit-runtime";
@@ -307,15 +307,6 @@ export class RequestClient {
}
}
function normalizeIntegerOption(
value: number | undefined,
fallback: number,
params: { min: number },
): number {
const candidate = parseFiniteNumber(value) ?? fallback;
return Math.max(params.min, Math.floor(candidate));
}
function normalizeRequestClientOptions(
options?: RequestClientOptions,
): NormalizedRequestClientOptions {
@@ -1,11 +1,11 @@
// Discord provider module implements model/runtime integration.
import { inspect } from "node:util";
import {
parseStrictFiniteNumber,
parseStrictNonNegativeInteger,
} from "openclaw/plugin-sdk/number-runtime";
import { formatDurationSeconds } from "openclaw/plugin-sdk/runtime-env";
import { formatErrorMessage } from "openclaw/plugin-sdk/ssrf-runtime";
import {
parseFiniteNumber as readFiniteNumber,
parseStrictNonNegativeInteger as readNonNegativeInteger,
} from "openclaw/plugin-sdk/string-coerce-runtime";
import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
import { RateLimitError } from "../internal/discord.js";
@@ -102,20 +102,6 @@ function readDiscordDeployObjectField(value: unknown, field: string): unknown {
: undefined;
}
function readFiniteNumber(value: unknown): number | undefined {
if (typeof value === "number" && Number.isFinite(value)) {
return value;
}
if (typeof value === "string" && value.trim().length > 0) {
return parseStrictFiniteNumber(value);
}
return undefined;
}
function readNonNegativeInteger(value: unknown): number | undefined {
return parseStrictNonNegativeInteger(value);
}
function isAbortLikeError(err: unknown): boolean {
if (!err || typeof err !== "object") {
return false;
+3 -7
View File
@@ -2,22 +2,18 @@ import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
import { makeProxyFetch } from "openclaw/plugin-sdk/fetch-runtime";
import { danger } from "openclaw/plugin-sdk/runtime-env";
import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env";
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
import type { ResolvedDiscordAccount } from "./accounts.js";
function resolveDiscordProxyUrl(
account: Pick<ResolvedDiscordAccount, "config">,
cfg: OpenClawConfig,
): string | undefined {
const accountProxy = account.config.proxy?.trim();
const accountProxy = normalizeOptionalString(account.config.proxy);
if (accountProxy) {
return accountProxy;
}
const channelProxy = cfg?.channels?.discord?.proxy;
if (typeof channelProxy !== "string") {
return undefined;
}
const trimmed = channelProxy.trim();
return trimmed || undefined;
return normalizeOptionalString(cfg?.channels?.discord?.proxy);
}
function resolveDiscordProxyFetchByUrl(
+5 -7
View File
@@ -1,6 +1,9 @@
// Duckduckgo helper module supports config behavior.
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
import {
normalizeLowercaseStringOrEmpty,
normalizeOptionalString,
} from "openclaw/plugin-sdk/string-coerce-runtime";
const DEFAULT_DDG_SAFE_SEARCH = "moderate";
@@ -25,12 +28,7 @@ function resolveDdgWebSearchConfig(
}
export function resolveDdgRegion(config?: OpenClawConfig): string | undefined {
const region = resolveDdgWebSearchConfig(config)?.region;
if (typeof region !== "string") {
return undefined;
}
const trimmed = region.trim();
return trimmed || undefined;
return normalizeOptionalString(resolveDdgWebSearchConfig(config)?.region);
}
export function resolveDdgSafeSearch(config?: OpenClawConfig): DdgSafeSearch {
@@ -9,7 +9,9 @@ import {
} from "openclaw/plugin-sdk/realtime-transcription";
import { normalizeResolvedSecretInputString } from "openclaw/plugin-sdk/secret-input";
import {
asFiniteNumberInRange,
asOptionalRecord as readRecord,
asSafeIntegerInRange,
normalizeOptionalString,
parseFiniteNumber as readFiniteNumber,
} from "openclaw/plugin-sdk/string-coerce-runtime";
@@ -81,19 +83,17 @@ function normalizeCommitStrategy(value: unknown): "manual" | "vad" | undefined {
function normalizePositiveSafeInteger(value: unknown): number | undefined {
const parsed = readFiniteNumber(value);
return parsed !== undefined && Number.isSafeInteger(parsed) && parsed > 0 ? parsed : undefined;
return asSafeIntegerInRange(parsed, { min: 1 });
}
function normalizeFiniteRange(value: unknown, min: number, max: number): number | undefined {
const parsed = readFiniteNumber(value);
return parsed !== undefined && parsed >= min && parsed <= max ? parsed : undefined;
return asFiniteNumberInRange(parsed, { min, max });
}
function normalizeIntegerRange(value: unknown, min: number, max: number): number | undefined {
const parsed = readFiniteNumber(value);
return parsed !== undefined && Number.isSafeInteger(parsed) && parsed >= min && parsed <= max
? parsed
: undefined;
return asSafeIntegerInRange(parsed, { min, max });
}
function normalizeProviderConfig(
+1 -4
View File
@@ -12,6 +12,7 @@ import {
resolveChannelPreviewStreamMode,
resolveChannelStreamingBlockEnabled,
} from "openclaw/plugin-sdk/channel-outbound";
import { toStringifiedError as toFeishuError } from "openclaw/plugin-sdk/error-runtime";
import { getGlobalHookRunner } from "openclaw/plugin-sdk/plugin-runtime";
import {
getReplyPayloadTtsSupplement,
@@ -77,10 +78,6 @@ function mergeStreamingFinalText(
return `${previousText}\n\n${nextText}`;
}
function toFeishuError(error: unknown): Error {
return error instanceof Error ? error : new Error(String(error));
}
/** Maximum age (ms) for a message to receive a typing indicator reaction.
* Messages older than this are likely replays after context compaction (#30418). */
const TYPING_INDICATOR_MAX_AGE_MS = 2 * 60_000;
@@ -3,6 +3,7 @@
import { formatByteSize } from "openclaw/plugin-sdk/number-runtime";
import { readPositiveIntegerParam } from "openclaw/plugin-sdk/param-readers";
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
type GatewayCallOptions = {
gatewayUrl?: string;
@@ -23,8 +24,7 @@ export function readGatewayCallOptions(params: Record<string, unknown>): Gateway
}
export function readTrimmedString(params: Record<string, unknown>, key: string): string {
const value = params[key];
return typeof value === "string" ? value.trim() : "";
return normalizeOptionalString(params[key]) ?? "";
}
export function readClampedInt(params: {
@@ -11,12 +11,11 @@ import type {
} from "openclaw/plugin-sdk/speech";
import {
asBoolean,
asFiniteNumber,
parseSpeechDirectiveNumberOverride,
resolveSpeechProviderApiKey,
trimToUndefined,
} from "openclaw/plugin-sdk/speech-core";
import { asOptionalRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
import { asFiniteNumberInRange, asOptionalRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
import {
FISH_AUDIO_STREAM_MAX_BYTES,
type FishAudioFormat,
@@ -71,8 +70,7 @@ function normalizeLatency(value: unknown): FishAudioLatency {
}
function normalizeNumber(value: unknown, min: number, max: number): number | undefined {
const number = asFiniteNumber(value);
return number != null && number >= min && number <= max ? number : undefined;
return asFiniteNumberInRange(value, { min, max });
}
function resolveReferenceId(raw: Record<string, unknown> | undefined): string | undefined {
+4 -12
View File
@@ -9,10 +9,12 @@ import {
type RealtimeVoiceAgentConsultToolPolicy,
} from "openclaw/plugin-sdk/realtime-voice";
import {
asBoolean,
asRecord,
normalizeOptionalLowercaseString,
normalizeOptionalString,
normalizeOptionalTrimmedStringList,
parseBooleanValue,
} from "openclaw/plugin-sdk/string-coerce-runtime";
export type GoogleMeetTransport = "chrome" | "chrome-node" | "twilio";
@@ -251,7 +253,7 @@ const GOOGLE_MEET_PREVIEW_ACK_KEYS = [
] as const;
function resolveBoolean(value: unknown, fallback: boolean): boolean {
return typeof value === "boolean" ? value : fallback;
return asBoolean(value) ?? fallback;
}
function resolveNumber(value: unknown, fallback: number): number {
@@ -289,17 +291,7 @@ function normalizeStringAllowEmpty(value: unknown): string | undefined {
}
function readEnvBoolean(env: NodeJS.ProcessEnv, keys: readonly string[]): boolean | undefined {
const normalized = normalizeOptionalLowercaseString(readEnvString(env, keys));
if (!normalized) {
return undefined;
}
if (["1", "true", "yes", "on"].includes(normalized)) {
return true;
}
if (["0", "false", "no", "off"].includes(normalized)) {
return false;
}
return undefined;
return parseBooleanValue(readEnvString(env, keys));
}
function readEnvNumber(env: NodeJS.ProcessEnv, keys: readonly string[]): number | undefined {
@@ -5,7 +5,12 @@ import type {
OpenClawPluginApi,
OpenClawPluginNodeInvokePolicy,
} from "openclaw/plugin-sdk/plugin-entry";
import { isRecord, normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
import {
asNonArrayRecord as asParamRecord,
normalizeOptionalString,
} from "openclaw/plugin-sdk/string-coerce-runtime";
export { asParamRecord };
import { isGoogleMeetBrowserManualActionError } from "./browser-manual-action-error.js";
import {
resolveGoogleMeetGatewayOperationTimeoutMs,
@@ -44,10 +49,6 @@ type LoadGoogleMeetNodeInvokePolicy = (
const loadGoogleMeetNodeInvokePolicy: LoadGoogleMeetNodeInvokePolicy = async (config) =>
(await loadGoogleMeetNodeInvokePolicyModule()).createGoogleMeetChromeNodeInvokePolicy(config);
export function asParamRecord(params: unknown): Record<string, unknown> {
return isRecord(params) ? params : {};
}
export function normalizeTransport(value: unknown): GoogleMeetTransport | undefined {
return value === "chrome" || value === "chrome-node" || value === "twilio" ? value : undefined;
}
+2 -2
View File
@@ -49,6 +49,7 @@ import { normalizeResolvedSecretInputString } from "openclaw/plugin-sdk/secret-i
import {
asBoolean,
asFiniteNumber,
asSafeIntegerInRange,
isRecord,
normalizeOptionalString,
} from "openclaw/plugin-sdk/string-coerce-runtime";
@@ -209,8 +210,7 @@ function asTurnCoverage(value: unknown): GoogleRealtimeTurnCoverage | undefined
}
function asNonNegativeInteger(value: unknown): number | undefined {
const number = asFiniteNumber(value);
return number !== undefined && Number.isSafeInteger(number) && number >= 0 ? number : undefined;
return asSafeIntegerInRange(value, { min: 0 });
}
function asGoogleRealtimeThinkingBudget(value: unknown): number | undefined {
@@ -6,6 +6,7 @@ import {
buildHostnameAllowlistPolicyFromSuffixAllowlist,
fetchWithSsrFGuard,
} from "openclaw/plugin-sdk/ssrf-runtime";
import { asNullableObjectRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
import { resolveUserPath } from "openclaw/plugin-sdk/text-utility-runtime";
import type { ResolvedGoogleChatAccount } from "./accounts.js";
import { MAX_GOOGLE_CHAT_SERVICE_ACCOUNT_FILE_BYTES } from "./google-auth-limits.js";
@@ -78,10 +79,6 @@ function installGoogleAuthHeaderCompatibilityInterceptor(
return transport;
}
function asNullableObjectRecord(value: unknown): Record<string, unknown> | null {
return value !== null && typeof value === "object" ? (value as Record<string, unknown>) : null;
}
function hasProxyAgentShape(value: unknown): value is ProxyAgentLike {
const record = asNullableObjectRecord(value);
return record !== null && record.proxy instanceof URL;
+2 -4
View File
@@ -1,6 +1,7 @@
// Imessage plugin module implements catchup behavior.
import { createHash } from "node:crypto";
import { KeyedAsyncQueue } from "openclaw/plugin-sdk/keyed-async-queue";
import { resolveIntegerOption } from "openclaw/plugin-sdk/number-runtime";
import type { PluginStateSyncKeyedStore } from "openclaw/plugin-sdk/plugin-state-runtime";
import { getIMessageRuntime } from "../runtime.js";
@@ -246,10 +247,7 @@ export type ResolvedCatchupConfig = {
};
function clampInt(value: number | undefined, min: number, max: number, fallback: number): number {
if (typeof value !== "number" || !Number.isFinite(value)) {
return fallback;
}
return Math.min(max, Math.max(min, Math.floor(value)));
return resolveIntegerOption(value, fallback, { min, max });
}
export function resolveCatchupConfig(
+2 -9
View File
@@ -10,6 +10,7 @@ import { getRuntimeConfig } from "openclaw/plugin-sdk/runtime-config-snapshot";
import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env";
import { detectBinary } from "openclaw/plugin-sdk/setup";
import {
filterStringEntries,
normalizeLowercaseStringOrEmpty,
normalizeStringEntries,
} from "openclaw/plugin-sdk/string-coerce-runtime";
@@ -187,14 +188,6 @@ function selectorsFromPayload(payload: Record<string, unknown>): Record<string,
return selectors;
}
function rpcMethodsFromPayload(payload: Record<string, unknown>): string[] {
const raw = payload.rpc_methods;
if (!Array.isArray(raw)) {
return [];
}
return raw.filter((entry): entry is string => typeof entry === "string");
}
// Probe whether the installed imsg CLI accepts `--file` on the `send-rich`
// subcommand (added by openclaw/imsg#114, which lets a single bridge call
// combine `--reply-to` and an attachment). We grep the help output rather
@@ -278,7 +271,7 @@ export async function probeIMessagePrivateApi(
}
const { payload, firstLineSnippet } = parseStatusPayload(result.stdout);
const selectors = payload ? selectorsFromPayload(payload) : {};
const rpcMethods = payload ? rpcMethodsFromPayload(payload) : [];
const rpcMethods = filterStringEntries(payload?.rpc_methods);
const advancedFeatures = payload?.advanced_features === true;
const v2Ready = payload?.v2_ready === true;
// imsg explains an unavailable bridge here (SIP, library validation, macOS
+2 -4
View File
@@ -1,6 +1,6 @@
import type { OpenClawPluginNodeHostCommandAvailabilityContext } from "openclaw/plugin-sdk/plugin-entry";
import type { CommandOptions, SpawnResult } from "openclaw/plugin-sdk/process-runtime";
import { asNonArrayRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
import { asFiniteNumber, asNonArrayRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
import {
resolveLinuxNodePluginConfigFromHost,
@@ -21,9 +21,7 @@ export function parseParams(paramsJSON: string | null | undefined): Record<strin
}
}
export function readFiniteNumber(value: unknown): number | undefined {
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
}
export { asFiniteNumber as readFiniteNumber };
export function clamp(value: number, minimum: number, maximum: number): number {
return Math.min(maximum, Math.max(minimum, value));
@@ -1,5 +1,6 @@
// Matrix plugin module implements env auth behavior.
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id";
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
import { getMatrixScopedEnvVarNames } from "../../env-vars.js";
type MatrixEnvConfig = {
@@ -12,7 +13,7 @@ type MatrixEnvConfig = {
};
function cleanEnv(value: unknown): string {
return typeof value === "string" ? value.trim() : "";
return normalizeOptionalString(value) ?? "";
}
export function resolveGlobalMatrixEnvConfig(env: NodeJS.ProcessEnv): MatrixEnvConfig {
@@ -1,5 +1,6 @@
// Matrix plugin module implements shared behavior.
import { normalizeOptionalAccountId } from "openclaw/plugin-sdk/account-id";
import { toStringifiedError as toRetirementError } from "openclaw/plugin-sdk/error-runtime";
import { createDeferred } from "openclaw/plugin-sdk/extension-shared";
import { createLazyRuntimeModule } from "openclaw/plugin-sdk/lazy-runtime";
import type { CoreConfig } from "../../types.js";
@@ -274,10 +275,6 @@ async function retireMonitorLeases(
}
}
function toRetirementError(error: unknown): Error {
return error instanceof Error ? error : new Error(String(error));
}
function mergeReleaseMode(
current: MatrixClientReleaseMode,
requested: MatrixClientReleaseMode,
@@ -1,6 +1,7 @@
// Matrix plugin module implements crypto bootstrap behavior.
import { setTimeout as sleep } from "node:timers/promises";
import { CryptoEvent } from "matrix-js-sdk/lib/crypto-api/CryptoEvent.js";
import { toStringifiedError } from "openclaw/plugin-sdk/error-runtime";
import type { MatrixDecryptBridge } from "./decrypt-bridge.js";
import { LogService } from "./logger.js";
import type { MatrixRecoveryKeyStore } from "./recovery-key-store.js";
@@ -236,7 +237,7 @@ export class MatrixCryptoBootstrapper<TRawEvent extends MatrixRawEvent> {
} catch (repairErr) {
LogService.warn("MatrixClientLite", "Forced cross-signing reset failed:", repairErr);
if (options.strict) {
throw repairErr instanceof Error ? repairErr : new Error(String(repairErr));
throw toStringifiedError(repairErr);
}
return { ready: false, published: false };
}
@@ -250,7 +251,7 @@ export class MatrixCryptoBootstrapper<TRawEvent extends MatrixRawEvent> {
{ cause: err },
);
}
throw err instanceof Error ? err : new Error(String(err));
throw toStringifiedError(err);
}
return { ready: false, published: false };
}
@@ -300,7 +301,7 @@ export class MatrixCryptoBootstrapper<TRawEvent extends MatrixRawEvent> {
} catch (resetErr) {
LogService.warn("MatrixClientLite", "Failed to bootstrap cross-signing:", resetErr);
if (options.strict) {
throw resetErr instanceof Error ? resetErr : new Error(String(resetErr));
throw toStringifiedError(resetErr);
}
return { ready: false, published: false };
}
@@ -328,7 +329,7 @@ export class MatrixCryptoBootstrapper<TRawEvent extends MatrixRawEvent> {
} catch (err) {
LogService.warn("MatrixClientLite", "Fallback cross-signing bootstrap failed:", err);
if (options.strict) {
throw err instanceof Error ? err : new Error(String(err));
throw toStringifiedError(err);
}
return { ready: false, published: false };
}
@@ -371,7 +372,7 @@ export class MatrixCryptoBootstrapper<TRawEvent extends MatrixRawEvent> {
} catch (err) {
LogService.warn("MatrixClientLite", "Failed to bootstrap secret storage:", err);
if (options.strict) {
throw err instanceof Error ? err : new Error(String(err));
throw toStringifiedError(err);
}
}
}
@@ -9,8 +9,10 @@ import { parseStrictInteger } from "openclaw/plugin-sdk/number-runtime";
import { normalizeProviderId } from "openclaw/plugin-sdk/provider-model-shared";
import { getSessionEntry, resolveStorePath } from "openclaw/plugin-sdk/session-store-runtime";
import {
asFiniteNumber,
normalizeOptionalString,
normalizeStringifiedOptionalString,
readStringField,
} from "openclaw/plugin-sdk/string-coerce-runtime";
import type { MattermostInteractiveButtonInput } from "./interactions.js";
@@ -59,19 +61,12 @@ function splitModelRef(modelRef?: string | null): { provider: string; model: str
}
function readContextString(context: Record<string, unknown>, key: string, fallback = ""): string {
const value = context[key];
return typeof value === "string" ? value : fallback;
return readStringField(context, key) ?? fallback;
}
function readContextNumber(context: Record<string, unknown>, key: string): number | undefined {
const value = context[key];
if (typeof value === "number" && Number.isFinite(value)) {
return value;
}
if (typeof value === "string") {
return parseStrictInteger(value);
}
return undefined;
return asFiniteNumber(value) ?? parseStrictInteger(value);
}
function normalizePage(value: number | undefined): number {
@@ -2,6 +2,7 @@
import { createHash } from "node:crypto";
import fs from "node:fs/promises";
import path from "node:path";
import { resolveNonNegativeIntegerOption } from "openclaw/plugin-sdk/number-runtime";
import type {
OpenKeyedStoreOptions,
PluginStateKeyedStore,
@@ -149,10 +150,6 @@ function normalizeImportRunEntries(value: unknown): ChatGptImportRunEntry[] {
});
}
function asNonNegativeInteger(value: unknown): number {
return typeof value === "number" && Number.isFinite(value) ? Math.max(0, Math.floor(value)) : 0;
}
function normalizeMemoryWikiImportRunRecord(raw: unknown): ChatGptImportRunRecord | null {
const record = asNullableRecord(raw);
if (!record) {
@@ -182,10 +179,10 @@ function normalizeMemoryWikiImportRunRecord(raw: unknown): ChatGptImportRunRecor
exportPath,
sourcePath,
appliedAt,
conversationCount: asNonNegativeInteger(record.conversationCount),
createdCount: asNonNegativeInteger(record.createdCount),
updatedCount: asNonNegativeInteger(record.updatedCount),
skippedCount: asNonNegativeInteger(record.skippedCount),
conversationCount: resolveNonNegativeIntegerOption(record.conversationCount, 0),
createdCount: resolveNonNegativeIntegerOption(record.createdCount, 0),
updatedCount: resolveNonNegativeIntegerOption(record.updatedCount, 0),
skippedCount: resolveNonNegativeIntegerOption(record.skippedCount, 0),
createdPaths: normalizeImportRunEntries(record.createdPaths),
updatedPaths: normalizeImportRunEntries(record.updatedPaths),
...(rollbackStartedAt ? { rollbackStartedAt } : {}),
+2 -3
View File
@@ -2,14 +2,13 @@
import type { StreamFn } from "openclaw/plugin-sdk/agent-core";
import type { ProviderWrapStreamFnContext } from "openclaw/plugin-sdk/plugin-entry";
import { createPayloadPatchStreamWrapper } from "openclaw/plugin-sdk/provider-stream-shared";
import { filterStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime";
const META_REASONING_ENCRYPTED_CONTENT_INCLUDE = "reasoning.encrypted_content";
function ensureMetaResponsesReplayFields(payloadObj: Record<string, unknown>): void {
const existing = payloadObj.include;
const include = Array.isArray(existing)
? existing.filter((entry): entry is string => typeof entry === "string")
: [];
const include = filterStringEntries(existing);
if (!include.includes(META_REASONING_ENCRYPTED_CONTENT_INCLUDE)) {
include.push(META_REASONING_ENCRYPTED_CONTENT_INCLUDE);
}
+1 -1
View File
@@ -248,7 +248,7 @@ function readOptionalTrimmedString(
params: Record<string, unknown>,
key: string,
): string | undefined {
return typeof params[key] === "string" ? params[key].trim() || undefined : undefined;
return normalizeOptionalString(params[key]);
}
function resolveActionUploadFilePath(params: Record<string, unknown>): string | undefined {
+1 -8
View File
@@ -31,13 +31,6 @@ function cacheRoomInfo(
pruneMapToMaxSize(roomCache, ROOM_CACHE_MAX_ENTRIES);
}
function coerceRoomType(value: unknown): number | undefined {
if (typeof value === "number" && Number.isSafeInteger(value) && value > 0) {
return value;
}
return parseStrictPositiveInteger(value);
}
function resolveRoomKindFromType(type: number | undefined): "direct" | "group" | undefined {
if (!type) {
return undefined;
@@ -117,7 +110,7 @@ export async function resolveNextcloudTalkRoomKind(params: {
const payload = await readProviderJsonResponse<{
ocs?: { data?: { type?: number | string } };
}>(response, "Nextcloud Talk room info failed");
const type = coerceRoomType(payload.ocs?.data?.type);
const type = parseStrictPositiveInteger(payload.ocs?.data?.type);
const kind = resolveRoomKindFromType(type);
cacheRoomInfo(key, { fetchedAt: Date.now(), kind });
return kind;
+1 -6
View File
@@ -22,7 +22,7 @@ import {
import { formatDocsLink } from "openclaw/plugin-sdk/setup-tools";
import {
normalizeLowercaseStringOrEmpty,
readStringValue,
readNonEmptyStringPreservingWhitespace as readNonEmptyUntrimmedString,
} from "openclaw/plugin-sdk/string-coerce-runtime";
import { resolveDefaultNextcloudTalkAccountId, resolveNextcloudTalkAccount } from "./accounts.js";
import type { CoreConfig } from "./types.js";
@@ -39,11 +39,6 @@ type NextcloudSetupInput = ChannelSetupInput & {
password?: string;
};
function readNonEmptyUntrimmedString(value: unknown): string | undefined {
const text = readStringValue(value);
return text ? text : undefined;
}
export function normalizeNextcloudTalkBaseUrl(value: string | undefined): string {
return value?.trim().replace(/\/+$/, "") ?? "";
}
+3 -4
View File
@@ -1,5 +1,8 @@
// Nostr plugin module owns durable ingress identity and legacy-state migration.
import type { ChannelIngressQueue } from "openclaw/plugin-sdk/channel-outbound";
import { isRecord as isNostrIngressRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
export { isNostrIngressRecord };
export const NOSTR_INGRESS_PAYLOAD_VERSION = 1;
@@ -20,10 +23,6 @@ export class NostrIngressPermanentError extends Error {
}
}
export function isNostrIngressRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function requiredString(value: unknown, field: string): string {
if (typeof value === "string" && value.trim()) {
return value;
@@ -5,6 +5,7 @@ import os from "node:os";
import path from "node:path";
import { DEFAULT_SECRET_FILE_MAX_BYTES, tryReadSecretFileSync } from "@openclaw/fs-safe/secret";
import { execa } from "execa";
import { coerceErrorMessage as errorMessage } from "openclaw/plugin-sdk/error-runtime";
import { resolveTrustedOnePasswordCli } from "./onepassword-op-path.js";
import { resolveOnePasswordSecretReference } from "./onepassword-secret-id.js";
@@ -65,10 +66,6 @@ function opMissingMessage(command) {
return `1Password CLI "${command}" is not installed or cannot be executed. Install the official 1Password CLI v2, and set CLAW_1PASSWORD_OP to its absolute path.`;
}
function errorMessage(error) {
return error instanceof Error ? error.message : String(error);
}
function resolveOsHome() {
const home =
process.platform === "win32"
+1 -4
View File
@@ -1,3 +1,4 @@
import { coerceErrorMessage as formatOpenAiBatchError } from "openclaw/plugin-sdk/error-runtime";
// Openai plugin module implements embedding batch behavior.
import {
applyEmbeddingBatchOutputLine,
@@ -158,10 +159,6 @@ async function fetchOpenAiBatchResource<T>(params: {
});
}
function formatOpenAiBatchError(error: unknown): string {
return error instanceof Error ? error.message : String(error);
}
function formatOpenAiBatchDiagnostic(error: unknown): string {
return formatBatchErrorDetail(formatOpenAiBatchError(error)) ?? "unknown error";
}
+2 -10
View File
@@ -35,6 +35,7 @@ import {
sanitizeConfiguredModelProviderRequest,
} from "openclaw/plugin-sdk/provider-http";
import { isPrivateNetworkOptInEnabled } from "openclaw/plugin-sdk/ssrf-runtime";
import { filterStringRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
import {
canonicalizeCodexResponsesBaseUrl,
@@ -447,16 +448,7 @@ function hasChatGPTImageRouteConfig(cfg: OpenClawConfig | undefined): boolean {
function resolveConfiguredOpenAIImageHeaders(
cfg: OpenClawConfig | undefined,
): Record<string, string> | undefined {
const headers = cfg?.models?.providers?.openai?.headers;
if (!headers) {
return undefined;
}
const stringHeaders = Object.fromEntries(
Object.entries(headers).filter(
(entry): entry is [string, string] => typeof entry[1] === "string",
),
);
return Object.keys(stringHeaders).length > 0 ? stringHeaders : undefined;
return filterStringRecord(cfg?.models?.providers?.openai?.headers);
}
function forceOpenAIImageApiKeyAuth(cfg: OpenClawConfig | undefined): OpenClawConfig | undefined {
@@ -14,7 +14,11 @@ import {
type RealtimeTranscriptionWebSocketTransport,
} from "openclaw/plugin-sdk/realtime-transcription";
import { normalizeResolvedSecretInputString } from "openclaw/plugin-sdk/secret-input";
import { asFiniteNumber, normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
import {
asFiniteNumberInRange,
asSafeIntegerInRange,
normalizeOptionalString,
} from "openclaw/plugin-sdk/string-coerce-runtime";
import {
createOpenAIRealtimeTranscriptionClientSecret,
readRealtimeErrorDetail,
@@ -133,19 +137,11 @@ function normalizeProviderConfig(
}
function normalizeNonNegativeInteger(value: unknown): number | undefined {
const number = asFiniteNumber(value);
if (number === undefined || !Number.isSafeInteger(number) || number < 0) {
return undefined;
}
return number;
return asSafeIntegerInRange(value, { min: 0 });
}
function normalizeVadThreshold(value: unknown): number | undefined {
const number = asFiniteNumber(value);
if (number === undefined || number < 0 || number > 1) {
return undefined;
}
return number;
return asFiniteNumberInRange(value, { min: 0, max: 1 });
}
function buildOpenAIRealtimeTranscriptionSessionPayload(
+12 -11
View File
@@ -2,6 +2,7 @@
import { execFileSync } from "node:child_process";
import { randomUUID } from "node:crypto";
import { resolveAgentDir } from "openclaw/plugin-sdk/agent-runtime";
import { toStringifiedError } from "openclaw/plugin-sdk/error-runtime";
import { canonicalizeBase64 } from "openclaw/plugin-sdk/media-runtime";
import type { PluginLogger } from "openclaw/plugin-sdk/plugin-entry";
import {
@@ -41,6 +42,8 @@ import {
} from "openclaw/plugin-sdk/secret-input";
import {
asFiniteNumber,
asFiniteNumberInRange,
asSafeIntegerInRange,
isRecord,
normalizeOptionalString,
} from "openclaw/plugin-sdk/string-coerce-runtime";
@@ -294,13 +297,11 @@ function normalizeProviderConfig(
}
function asNonNegativeInteger(value: unknown): number | undefined {
const number = asFiniteNumber(value);
return number !== undefined && Number.isSafeInteger(number) && number >= 0 ? number : undefined;
return asSafeIntegerInRange(value, { min: 0 });
}
function asUnitInterval(value: unknown): number | undefined {
const number = asFiniteNumber(value);
return number !== undefined && number >= 0 && number <= 1 ? number : undefined;
return asFiniteNumberInRange(value, { min: 0, max: 1 });
}
type OpenAIRealtimeApiKeyResolution =
@@ -995,7 +996,7 @@ class OpenAIRealtimeVoiceBridge implements RealtimeVoiceBridge {
try {
this.handleEvent(event, lifecycleConnection);
} catch (error) {
const readyError = error instanceof Error ? error : new Error(String(error));
const readyError = toStringifiedError(error);
attempt.reject(readyError);
this.failConnection(readyError, ws, lifecycleConnection, {
code: 1011,
@@ -1036,7 +1037,7 @@ class OpenAIRealtimeVoiceBridge implements RealtimeVoiceBridge {
},
});
if (!attempt.ready) {
const startupError = error instanceof Error ? error : new Error(String(error));
const startupError = toStringifiedError(error);
rejectStartup(
isDirectOpenAIRealtimeWebSocketUrl(url) &&
isOpenAIRealtimeStartupAuthFailure(startupError)
@@ -1045,7 +1046,7 @@ class OpenAIRealtimeVoiceBridge implements RealtimeVoiceBridge {
);
return;
}
this.config.onError?.(error instanceof Error ? error : new Error(String(error)));
this.config.onError?.(toStringifiedError(error));
});
ws.on("close", (code, reasonBuffer) => {
@@ -1091,7 +1092,7 @@ class OpenAIRealtimeVoiceBridge implements RealtimeVoiceBridge {
try {
connectionOrPromise = this.resolveConnectionParams();
} catch (error) {
attempt.reject(error instanceof Error ? error : new Error(String(error)));
attempt.reject(toStringifiedError(error));
return attempt.promise;
}
if (connectionOrPromise instanceof Promise) {
@@ -1103,13 +1104,13 @@ class OpenAIRealtimeVoiceBridge implements RealtimeVoiceBridge {
attempt.resolve();
return;
}
attempt.reject(error instanceof Error ? error : new Error(String(error)));
attempt.reject(toStringifiedError(error));
});
} else {
try {
openWebSocket(connectionOrPromise);
} catch (error) {
attempt.reject(error instanceof Error ? error : new Error(String(error)));
attempt.reject(toStringifiedError(error));
}
}
await attempt.promise;
@@ -1277,7 +1278,7 @@ class OpenAIRealtimeVoiceBridge implements RealtimeVoiceBridge {
if (!this.lifecycle.acceptsEvents(nextConnection)) {
return;
}
this.config.onError?.(error instanceof Error ? error : new Error(String(error)));
this.config.onError?.(toStringifiedError(error));
await this.attemptReconnect(reason, nextConnection);
}
}
@@ -11,6 +11,7 @@ import {
toImageDataUrl,
} from "openclaw/plugin-sdk/image-generation";
import { resolveGeneratedMediaMaxBytes } from "openclaw/plugin-sdk/media-generation-runtime";
import { resolveIntegerOption } from "openclaw/plugin-sdk/number-runtime";
import { isProviderApiKeyConfigured } from "openclaw/plugin-sdk/provider-auth";
import { resolveApiKeyForProvider } from "openclaw/plugin-sdk/provider-auth-runtime";
import {
@@ -171,10 +172,7 @@ function extractOpenRouterImagesFromResponse(body: unknown): GeneratedImageAsset
}
function resolveImageCount(count: number | undefined): number {
if (typeof count !== "number" || !Number.isFinite(count)) {
return 1;
}
return Math.max(1, Math.min(MAX_IMAGE_RESULTS, Math.trunc(count)));
return resolveIntegerOption(count, 1, { min: 1, max: MAX_IMAGE_RESULTS });
}
function isGeminiImageModel(model: string): boolean {
+2 -4
View File
@@ -19,6 +19,7 @@ import {
import {
asOptionalRecord,
asPositiveSafeInteger,
filterStringEntries,
normalizeOptionalString,
} from "openclaw/plugin-sdk/string-coerce-runtime";
@@ -140,10 +141,7 @@ export function buildOpenrouterProvider(): ModelProviderConfig {
}
function readStringArray(record: Record<string, unknown> | undefined, key: string): string[] {
const value = record?.[key];
return Array.isArray(value)
? value.filter((entry): entry is string => typeof entry === "string")
: [];
return filterStringEntries(record?.[key]);
}
function readTokenPrice(record: Record<string, unknown> | undefined, key: string): number {
+7 -9
View File
@@ -57,10 +57,6 @@ type OpenRouterVideoRequestPolicyCacheKey = ReturnType<
type OpenRouterVideoRequestConfig = Parameters<typeof sanitizeConfiguredModelProviderRequest>[0];
function normalizeStringArray(value: unknown): string[] {
return normalizeTrimmedStringList(value);
}
function normalizeNumberArray(value: unknown): number[] {
return Array.isArray(value)
? value.filter((entry): entry is number => typeof entry === "number" && Number.isFinite(entry))
@@ -68,14 +64,14 @@ function normalizeNumberArray(value: unknown): number[] {
}
function normalizeResolutionArray(value: unknown): VideoGenerationResolution[] {
return normalizeStringArray(value).map(
return normalizeTrimmedStringList(value).map(
(entry) => entry.toUpperCase() as VideoGenerationResolution,
);
}
function normalizeFrameImageRoles(value: unknown): Array<"first_frame" | "last_frame"> {
const seen = new Set<"first_frame" | "last_frame">();
for (const entry of normalizeStringArray(value)) {
for (const entry of normalizeTrimmedStringList(value)) {
if (entry === "first_frame" || entry === "last_frame") {
seen.add(entry);
}
@@ -136,12 +132,14 @@ function buildOpenRouterVideoModeCapabilities(params: {
function buildOpenRouterVideoModelCapabilities(
model: OpenRouterVideoModel,
): OpenRouterVideoModelCatalogCapabilities {
const aspectRatios = normalizeStringArray(model.supported_aspect_ratios);
const aspectRatios = normalizeTrimmedStringList(model.supported_aspect_ratios);
const durations = normalizeNumberArray(model.supported_durations);
const frameImages = normalizeFrameImageRoles(model.supported_frame_images);
const resolutions = normalizeResolutionArray(model.supported_resolutions);
const sizes = normalizeStringArray(model.supported_sizes);
const allowedPassthroughParameters = normalizeStringArray(model.allowed_passthrough_parameters);
const sizes = normalizeTrimmedStringList(model.supported_sizes);
const allowedPassthroughParameters = normalizeTrimmedStringList(
model.allowed_passthrough_parameters,
);
const supportsAudio =
typeof model.generate_audio === "boolean" ? model.generate_audio : undefined;
const modeCapabilities = buildOpenRouterVideoModeCapabilities({
@@ -1,3 +1,4 @@
import { resolveIntegerOption } from "openclaw/plugin-sdk/number-runtime";
// Transport-agnostic Parallel search normalization shared by the paid REST
// provider (`parallel`) and the free Search MCP provider (`parallel-free`).
// Both transports return the same v1 result shape, so query/result handling
@@ -53,7 +54,10 @@ export function resolveParallelSearchCount(
const value =
requestedCount ??
(typeof configuredCount === "number" ? configuredCount : DEFAULT_SEARCH_COUNT);
return Math.max(1, Math.min(PARALLEL_MAX_SEARCH_COUNT, Math.floor(value)));
return resolveIntegerOption(value, DEFAULT_SEARCH_COUNT, {
min: 1,
max: PARALLEL_MAX_SEARCH_COUNT,
});
}
export function normalizeParallelSessionId(
+4 -7
View File
@@ -5,7 +5,10 @@ import path from "node:path";
import { StringDecoder } from "node:string_decoder";
import { pathToFileURL } from "node:url";
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
import { readStringValue } from "openclaw/plugin-sdk/string-coerce-runtime";
import {
asNullableRecord as readRecord,
readStringValue,
} from "openclaw/plugin-sdk/string-coerce-runtime";
import pLimit from "p-limit";
import type {
QaEvidenceArtifactView,
@@ -565,12 +568,6 @@ async function buildArtifactView(params: {
};
}
function readRecord(value: unknown): Record<string, unknown> | null {
return value && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: null;
}
function readCountRecord(value: unknown): Record<string, number> {
const record = readRecord(value);
if (!record) {
@@ -1,4 +1,5 @@
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
import { toStringifiedError } from "openclaw/plugin-sdk/error-runtime";
import type { QaRunnerCliRegistration } from "openclaw/plugin-sdk/qa-runner-runtime";
import {
acquireQaCredentialLease,
@@ -96,7 +97,7 @@ export async function createDiscordQaTransportAdapter(
}
})().catch((error: unknown) => {
if (!stopped) {
pollingError = error instanceof Error ? error : new Error(String(error));
pollingError = toStringifiedError(error);
}
});
const scenarioEnvironment = createDiscordQaScenarioEnvironment({
@@ -3,6 +3,7 @@ import { randomUUID } from "node:crypto";
import path from "node:path";
import { setTimeout as sleep } from "node:timers/promises";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
import { toStringifiedError } from "openclaw/plugin-sdk/error-runtime";
import { buildQaTarget } from "openclaw/plugin-sdk/qa-channel-protocol";
import type { QaRunnerCliRegistration } from "openclaw/plugin-sdk/qa-runner-runtime";
import { readQaScenarioExecutionConfig } from "../../scenario-catalog.js";
@@ -324,7 +325,7 @@ export async function createMatrixQaTransportAdapter(
}),
).catch((error: unknown) => {
if (!stopped) {
pollingError = error instanceof Error ? error : new Error(String(error));
pollingError = toStringifiedError(error);
}
});
@@ -8,6 +8,7 @@ import {
} from "@openclaw/slack/api.js";
import type { FetchFunction } from "@slack/web-api";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
import { toStringifiedError } from "openclaw/plugin-sdk/error-runtime";
import { acquireDebugProxyCaptureStore } from "openclaw/plugin-sdk/proxy-capture";
import type { QaRunnerCliRegistration } from "openclaw/plugin-sdk/qa-runner-runtime";
import {
@@ -222,7 +223,7 @@ export async function createSlackQaTransportAdapter(
}
})().catch((error: unknown) => {
if (!stopped) {
pollingError = error instanceof Error ? error : new Error(String(error));
pollingError = toStringifiedError(error);
}
});
};
@@ -3,6 +3,7 @@ import fs from "node:fs/promises";
import path from "node:path";
import type { WhatsAppQaDriverSession } from "@openclaw/whatsapp/api.js";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
import { toStringifiedError } from "openclaw/plugin-sdk/error-runtime";
import { buildQaTarget } from "openclaw/plugin-sdk/qa-channel-protocol";
import type { QaRunnerCliRegistration } from "openclaw/plugin-sdk/qa-runner-runtime";
import { resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk/temp-path";
@@ -130,7 +131,7 @@ export async function createWhatsAppQaTransportAdapter(
}
})().catch((error: unknown) => {
if (!stopped) {
pollingError = error instanceof Error ? error : new Error(String(error));
pollingError = toStringifiedError(error);
}
});
+1 -4
View File
@@ -1,6 +1,7 @@
// Qa Lab plugin module implements process tree cpu behavior.
import { spawnSync } from "node:child_process";
import { parseStrictFiniteNumber, parseStrictInteger } from "openclaw/plugin-sdk/number-runtime";
import { isRecord as isPlainObject } from "openclaw/plugin-sdk/string-coerce-runtime";
import { resolveQaWindowsPowerShellExePath } from "./windows-system-tools.js";
type ProcessTreeSnapshot = {
@@ -11,10 +12,6 @@ type ProcessTreeSnapshot = {
const PROCESS_TREE_SNAPSHOT_TIMEOUT_MS = 5_000;
function isPlainObject(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function parsePositiveInteger(value: unknown): number | null {
const parsed = parseStrictInteger(value);
if (parsed === undefined || parsed <= 0) {
+1 -4
View File
@@ -2,6 +2,7 @@
import { spawn, spawnSync, type ChildProcessWithoutNullStreams } from "node:child_process";
import path from "node:path";
import { resolveTimerTimeoutMs } from "openclaw/plugin-sdk/number-runtime";
import { isRecord as isJsonRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
import {
appendQaChildOutput,
@@ -83,10 +84,6 @@ function parseBalancedJsonPayloadStart(text: string) {
}
}
function isJsonRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value && typeof value === "object" && !Array.isArray(value));
}
function isStructuredDiagnosticJson(value: unknown) {
if (!isJsonRecord(value)) {
return false;
+19 -22
View File
@@ -394,12 +394,8 @@ async function formatRuntimePatchMutationDiagnostics(params: {
].join("; ");
}
function readNonEmptyString(value: unknown) {
return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
}
function normalizeToolCallId(value: unknown) {
return readNonEmptyString(value);
return normalizeOptionalString(value);
}
function stringifyTranscriptToolResult(value: unknown): string {
@@ -436,10 +432,10 @@ function extractTranscriptText(value: unknown): string {
continue;
}
const text =
readNonEmptyString(block.text) ??
readNonEmptyString(block.content) ??
readNonEmptyString(block.message) ??
readNonEmptyString(block.error);
normalizeOptionalString(block.text) ??
normalizeOptionalString(block.content) ??
normalizeOptionalString(block.message) ??
normalizeOptionalString(block.error);
if (text) {
parts.push(text);
}
@@ -457,11 +453,11 @@ function extractTranscriptToolCalls(
if (!isRecord(block)) {
continue;
}
const type = readNonEmptyString(block.type)?.toLowerCase();
const type = normalizeOptionalString(block.type)?.toLowerCase();
if (type !== "tool_use" && type !== "toolcall" && type !== "tool_call") {
continue;
}
const tool = readNonEmptyString(block.name);
const tool = normalizeOptionalString(block.name);
if (!tool) {
continue;
}
@@ -486,7 +482,8 @@ function extractTranscriptToolCalls(
continue;
}
const functionRecord = isRecord(call.function) ? call.function : undefined;
const tool = readNonEmptyString(call.name) ?? readNonEmptyString(functionRecord?.name);
const tool =
normalizeOptionalString(call.name) ?? normalizeOptionalString(functionRecord?.name);
if (!tool) {
continue;
}
@@ -558,10 +555,10 @@ function extractTranscriptToolResults(
): QaRuntimeToolFixtureTranscriptToolResult[] {
const results: QaRuntimeToolFixtureTranscriptToolResult[] = [];
const tool =
readNonEmptyString(message.toolName) ??
readNonEmptyString(message.tool_name) ??
readNonEmptyString(message.name) ??
readNonEmptyString(message.tool);
normalizeOptionalString(message.toolName) ??
normalizeOptionalString(message.tool_name) ??
normalizeOptionalString(message.name) ??
normalizeOptionalString(message.tool);
if ((message.role === "tool" || message.role === "toolResult") && message.content !== undefined) {
const text = extractTranscriptText(message.content);
const structuredFailure = isStructuredFailureToolResult({
@@ -598,7 +595,7 @@ function extractTranscriptToolResults(
if (!isRecord(block)) {
continue;
}
const type = readNonEmptyString(block.type)?.toLowerCase();
const type = normalizeOptionalString(block.type)?.toLowerCase();
if (type !== "tool_result" && type !== "toolresult" && type !== "tool_result_error") {
continue;
}
@@ -611,10 +608,10 @@ function extractTranscriptToolResults(
is_error: block.is_error,
});
const blockTool =
readNonEmptyString(block.toolName) ??
readNonEmptyString(block.tool_name) ??
readNonEmptyString(block.name) ??
readNonEmptyString(block.tool);
normalizeOptionalString(block.toolName) ??
normalizeOptionalString(block.tool_name) ??
normalizeOptionalString(block.name) ??
normalizeOptionalString(block.tool);
results.push({
id:
normalizeToolCallId(block.tool_use_id) ??
@@ -703,7 +700,7 @@ async function readSessionTranscriptBytes(
) {
const store = await readRawQaSessionStore(env);
const entry = store[sessionKey];
const sessionId = readNonEmptyString(entry?.sessionId);
const sessionId = normalizeOptionalString(entry?.sessionId);
if (!sessionId) {
throw new Error(`session transcript entry not found for ${sessionKey}`);
}
+1 -4
View File
@@ -1,3 +1,4 @@
import { normalizeOptionalString as normalizeQaConfigString } from "openclaw/plugin-sdk/string-coerce-runtime";
import type { QaCliBackendAuthMode } from "./gateway-child.js";
import { splitQaModelRef, type QaProviderMode } from "./model-selection.js";
import {
@@ -14,10 +15,6 @@ export type QaScenarioExecutionCell = {
channel: string | null;
};
function normalizeQaConfigString(value: unknown): string | undefined {
return typeof value === "string" && value.trim() ? value.trim() : undefined;
}
function resolveQaScenarioLaneChannels(params: {
scenario: QaSeedScenario;
channelDriver: QaScorecardChannelDriver;
+1 -4
View File
@@ -1,3 +1,4 @@
import { toStringifiedError as asError } from "openclaw/plugin-sdk/error-runtime";
import { buildTimeoutAbortSignal } from "openclaw/plugin-sdk/extension-shared";
import { readProviderJsonResponse } from "openclaw/plugin-sdk/provider-http";
import WebSocket from "ws";
@@ -590,10 +591,6 @@ export class ReefInboxConnection {
}
}
function asError(error: unknown): Error {
return error instanceof Error ? error : new Error(String(error));
}
function reefInboxCloseError(event: { code?: number; reason?: string }): Error {
const code = Number.isInteger(event.code) ? ` code=${event.code}` : "";
const reason = event.reason?.trim() ? ` reason=${event.reason.trim()}` : "";
+3 -10
View File
@@ -4,6 +4,7 @@ import {
normalizeResolvedSecretInputString,
normalizeSecretInput,
} from "openclaw/plugin-sdk/secret-input";
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
type SearxngPluginConfig = {
webSearch?: {
@@ -37,14 +38,6 @@ function readInlineEnvSecretRefValue(value: unknown, env: NodeJS.ProcessEnv): st
return normalizeSecretInput(env[record.id]);
}
function normalizeTrimmedString(value: unknown): string | undefined {
if (typeof value !== "string") {
return undefined;
}
const trimmed = value.trim();
return trimmed || undefined;
}
function normalizeBaseUrl(value: string | undefined): string | undefined {
return value?.replace(/\/+$/u, "") || undefined;
}
@@ -78,9 +71,9 @@ export function resolveSearxngBaseUrl(
}
export function resolveSearxngCategories(config?: OpenClawConfig): string | undefined {
return normalizeTrimmedString(resolveSearxngWebSearchConfig(config)?.categories);
return normalizeOptionalString(resolveSearxngWebSearchConfig(config)?.categories);
}
export function resolveSearxngLanguage(config?: OpenClawConfig): string | undefined {
return normalizeTrimmedString(resolveSearxngWebSearchConfig(config)?.language);
return normalizeOptionalString(resolveSearxngWebSearchConfig(config)?.language);
}
+4 -5
View File
@@ -4,7 +4,10 @@ import {
renderMessagePresentationTableFallbackText,
type MessagePresentationTableBlock,
} from "openclaw/plugin-sdk/interactive-runtime";
import { asOptionalRecord, hasNonEmptyString } from "openclaw/plugin-sdk/string-coerce-runtime";
import {
asOptionalRecord,
readNonBlankString as readNonEmptyString,
} from "openclaw/plugin-sdk/string-coerce-runtime";
import { escapeSlackMrkdwn } from "./monitor/mrkdwn.js";
import { renderSlackMessagePresentationTableFallbackText } from "./presentation-fallback.js";
@@ -44,10 +47,6 @@ type ParsedSlackDataTable = {
cellCharacterCount: number;
};
function readNonEmptyString(value: unknown): string | undefined {
return hasNonEmptyString(value) ? value : undefined;
}
function countCharacters(value: string): number {
return Array.from(value).length;
}
@@ -18,6 +18,7 @@ import {
} from "openclaw/plugin-sdk/number-runtime";
import { classifyTransientNetworkErrorCode } from "openclaw/plugin-sdk/retry-runtime";
import { logVerbose, shouldLogVerbose } from "openclaw/plugin-sdk/runtime-env";
import { normalizeOptionalString as normalizeThreadTs } from "openclaw/plugin-sdk/string-coerce-runtime";
import { formatSlackError } from "../errors.js";
import type { SlackMessageEvent } from "../types.js";
import type { SlackIngressTurnLifecycle } from "./ingress.js";
@@ -30,11 +31,6 @@ type ThreadTsCacheEntry = {
const DEFAULT_THREAD_TS_CACHE_TTL_MS = 60_000;
const DEFAULT_THREAD_TS_CACHE_MAX = 500;
const normalizeThreadTs = (threadTs?: string | null) => {
const trimmed = threadTs?.trim();
return trimmed ? trimmed : undefined;
};
const markAmbiguousThreadReply = (message: SlackMessageEvent): SlackMessageEvent => ({
...message,
_ambiguousThreadReply: true,
+3 -6
View File
@@ -6,6 +6,7 @@ import type { SpeechVoiceOption } from "openclaw/plugin-sdk/speech";
import {
normalizeLowercaseStringOrEmpty,
normalizeOptionalLowercaseString,
normalizeOptionalString,
} from "openclaw/plugin-sdk/string-coerce-runtime";
import { resolveActiveTalkProviderConfig } from "openclaw/plugin-sdk/talk-config-runtime";
import { definePluginEntry, type OpenClawPluginApi } from "./api.js";
@@ -95,11 +96,7 @@ function findVoice(voices: SpeechVoiceOption[], query: string): SpeechVoiceOptio
}
function asTrimmedString(value: unknown): string {
return typeof value === "string" ? value.trim() : "";
}
function parsePositiveIntegerToken(value: unknown): number | undefined {
return parseStrictPositiveInteger(value);
return normalizeOptionalString(value) ?? "";
}
function resolveCommandLabel(channel: string): string {
@@ -171,7 +168,7 @@ export default definePluginEntry({
}
if (action === "list") {
const limit = parsePositiveIntegerToken(tokens[1]) ?? 12;
const limit = parseStrictPositiveInteger(tokens[1]) ?? 12;
try {
const voices = await api.runtime.tts.listVoices({
provider: providerId,
+9 -16
View File
@@ -9,6 +9,7 @@
* without requiring a gateway restart.
*/
import { filterStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime";
import type { UrbitSSEClient } from "./urbit/sse-client.js";
/** Pending approval request stored for persistence */
@@ -112,10 +113,10 @@ function parseSettingsResponse(raw: unknown): TlonSettingsStore {
return {
groupChannels: Array.isArray(settings.groupChannels)
? settings.groupChannels.filter((x): x is string => typeof x === "string")
? filterStringEntries(settings.groupChannels)
: undefined,
dmAllowlist: Array.isArray(settings.dmAllowlist)
? settings.dmAllowlist.filter((x): x is string => typeof x === "string")
? filterStringEntries(settings.dmAllowlist)
: undefined,
autoDiscoverChannels:
typeof settings.autoDiscoverChannels === "boolean"
@@ -129,11 +130,11 @@ function parseSettingsResponse(raw: unknown): TlonSettingsStore {
? settings.autoAcceptGroupInvites
: undefined,
groupInviteAllowlist: Array.isArray(settings.groupInviteAllowlist)
? settings.groupInviteAllowlist.filter((x): x is string => typeof x === "string")
? filterStringEntries(settings.groupInviteAllowlist)
: undefined,
channelRules: parseChannelRules(settings.channelRules),
defaultAuthorizedShips: Array.isArray(settings.defaultAuthorizedShips)
? settings.defaultAuthorizedShips.filter((x): x is string => typeof x === "string")
? filterStringEntries(settings.defaultAuthorizedShips)
: undefined,
ownerShip: typeof settings.ownerShip === "string" ? settings.ownerShip : undefined,
pendingApprovals: parsePendingApprovals(settings.pendingApprovals),
@@ -242,14 +243,10 @@ function applySettingsUpdate(
switch (key) {
case "groupChannels":
next.groupChannels = Array.isArray(value)
? value.filter((x): x is string => typeof x === "string")
: undefined;
next.groupChannels = Array.isArray(value) ? filterStringEntries(value) : undefined;
break;
case "dmAllowlist":
next.dmAllowlist = Array.isArray(value)
? value.filter((x): x is string => typeof x === "string")
: undefined;
next.dmAllowlist = Array.isArray(value) ? filterStringEntries(value) : undefined;
break;
case "autoDiscoverChannels":
next.autoDiscoverChannels = typeof value === "boolean" ? value : undefined;
@@ -264,17 +261,13 @@ function applySettingsUpdate(
next.autoAcceptGroupInvites = typeof value === "boolean" ? value : undefined;
break;
case "groupInviteAllowlist":
next.groupInviteAllowlist = Array.isArray(value)
? value.filter((x): x is string => typeof x === "string")
: undefined;
next.groupInviteAllowlist = Array.isArray(value) ? filterStringEntries(value) : undefined;
break;
case "channelRules":
next.channelRules = parseChannelRules(value);
break;
case "defaultAuthorizedShips":
next.defaultAuthorizedShips = Array.isArray(value)
? value.filter((x): x is string => typeof x === "string")
: undefined;
next.defaultAuthorizedShips = Array.isArray(value) ? filterStringEntries(value) : undefined;
break;
case "ownerShip":
next.ownerShip = typeof value === "string" ? value : undefined;
+2 -16
View File
@@ -14,7 +14,7 @@ import type {
SpeechSynthesisRequest,
SpeechTelephonySynthesisRequest,
} from "openclaw/plugin-sdk/speech-core";
import { asOptionalRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
import { asOptionalRecord, filterStringRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
import { tempWorkspace, resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk/temp-path";
import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
@@ -41,20 +41,6 @@ function asStringArray(value: unknown): string[] | undefined {
return Array.isArray(value) && value.every((v) => typeof v === "string") ? value : undefined;
}
function readStringRecord(value: unknown): Record<string, string> | undefined {
const obj = asOptionalRecord(value);
if (!obj) {
return undefined;
}
const result: Record<string, string> = {};
for (const [k, v] of Object.entries(obj)) {
if (typeof v === "string") {
result[k] = v;
}
}
return Object.keys(result).length > 0 ? result : undefined;
}
function normalizeOutputFormat(value: unknown): OutputFormat {
if (typeof value !== "string") {
return "mp3";
@@ -82,7 +68,7 @@ function getConfig(cfg: SpeechProviderConfig): CliConfig | null {
outputFormat: normalizeOutputFormat(cfg.outputFormat),
timeoutMs: typeof cfg.timeoutMs === "number" ? cfg.timeoutMs : DEFAULT_TIMEOUT_MS,
cwd: typeof cfg.cwd === "string" ? cfg.cwd : undefined,
env: readStringRecord(cfg.env),
env: filterStringRecord(cfg.env),
};
}
+1 -5
View File
@@ -4,7 +4,7 @@ import { ErrorCodes, errorShape } from "openclaw/plugin-sdk/gateway-runtime";
import { resolveGlobalSingleton } from "openclaw/plugin-sdk/global-singleton";
import { normalizeAgentId, parseAgentSessionKey } from "openclaw/plugin-sdk/routing";
import {
asNonArrayRecord,
asNonArrayRecord as asParamRecord,
asOptionalRecord,
normalizeOptionalString,
} from "openclaw/plugin-sdk/string-coerce-runtime";
@@ -88,10 +88,6 @@ const VoiceCallToolSchema = Type.Union([
}),
]);
function asParamRecord(params: unknown): Record<string, unknown> {
return asNonArrayRecord(params);
}
function isCliOnlyProcess(): boolean {
return process.env.OPENCLAW_CLI === "1" && !process.argv.slice(2).includes("gateway");
}
@@ -5,11 +5,6 @@ import {
readStringField,
} from "openclaw/plugin-sdk/string-coerce-runtime";
/** Read finite numeric config values. */
function getNumber(obj: Record<string, unknown> | undefined, key: string): number | undefined {
return asFiniteNumber(obj?.[key]);
}
/** Merge legacy provider-specific values into the canonical providers map. */
function mergeProviderConfig(
providersValue: unknown,
@@ -55,11 +50,11 @@ export function migrateVoiceCallLegacyConfigInput(params: {
if (streamingSttModel) {
legacyStreamingOpenAICompat.model = streamingSttModel;
}
const streamingSilenceDurationMs = getNumber(streaming, "silenceDurationMs");
const streamingSilenceDurationMs = asFiniteNumber(streaming?.silenceDurationMs);
if (streamingSilenceDurationMs !== undefined) {
legacyStreamingOpenAICompat.silenceDurationMs = streamingSilenceDurationMs;
}
const streamingVadThreshold = getNumber(streaming, "vadThreshold");
const streamingVadThreshold = asFiniteNumber(streaming?.vadThreshold);
if (streamingVadThreshold !== undefined) {
legacyStreamingOpenAICompat.vadThreshold = streamingVadThreshold;
}
@@ -138,14 +133,14 @@ export function migrateVoiceCallLegacyConfigInput(params: {
`Moved ${configPathPrefix}.streaming.sttModel → ${configPathPrefix}.streaming.providers.openai.model.`,
);
}
if (getNumber(streaming, "silenceDurationMs") !== undefined) {
if (asFiniteNumber(streaming?.silenceDurationMs) !== undefined) {
changes.push(
`Moved ${configPathPrefix}.streaming.silenceDurationMs → ${configPathPrefix}.streaming.providers.openai.silenceDurationMs.`,
);
} else if (typeof streaming?.silenceDurationMs === "number") {
changes.push(`Removed invalid ${configPathPrefix}.streaming.silenceDurationMs.`);
}
if (getNumber(streaming, "vadThreshold") !== undefined) {
if (asFiniteNumber(streaming?.vadThreshold) !== undefined) {
changes.push(
`Moved ${configPathPrefix}.streaming.vadThreshold → ${configPathPrefix}.streaming.providers.openai.vadThreshold.`,
);
@@ -12,6 +12,7 @@ import {
} from "openclaw/plugin-sdk/model-session-runtime";
import {
isRecord,
filterStringEntries,
normalizeLowercaseStringOrEmpty,
normalizeStringEntries,
} from "openclaw/plugin-sdk/string-coerce-runtime";
@@ -66,7 +67,7 @@ function readExplicitToolsAllow(value: unknown): string[] | undefined {
return undefined;
}
return allow.filter((entry): entry is string => typeof entry === "string");
return filterStringEntries(allow);
}
function resolveVoiceAgentToolsAllow(config: CoreConfig, agentId: string): string[] | undefined {
@@ -14,7 +14,10 @@ import {
} from "openclaw/plugin-sdk/channel-outbound";
import { buildInboundHistoryFromEntries } from "openclaw/plugin-sdk/reply-history";
import type { FinalizedMsgContext } from "openclaw/plugin-sdk/reply-runtime";
import { normalizeStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime";
import {
normalizeOptionalString,
normalizeStringEntries,
} from "openclaw/plugin-sdk/string-coerce-runtime";
import { requireWhatsAppInboundAdmission } from "../../inbound/admission.js";
import type { AdmittedWebInboundMessage } from "../../inbound/types.js";
import {
@@ -147,7 +150,7 @@ function isWhatsAppVisibleDeliveryError(error: unknown): boolean {
}
function readTrimmedString(value: unknown): string {
return typeof value === "string" ? value.trim() : "";
return normalizeOptionalString(value) ?? "";
}
function markWhatsAppReplyDeliveryErrorVisibleAfterFlush(

Some files were not shown because too many files have changed in this diff Show More