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

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