refactor: consolidate coercion contracts (#122458)

* refactor: consolidate coercion contracts

Centralize exact string, record, numeric, date, Boolean, argument, and structured-error coercions while preserving call-site semantics.

Migrate canonical-name collisions and deprecated internal SDK bypasses, deleting 55 net production/tooling lines. Expand declaration ownership enforcement to 101 allowed helpers and add a narrow export-completeness audit.

* fix: preserve standalone script coercions

Keep copied Control UI tooling self-contained and retain the trusted release harness module-relative source seam when the harness runs against an old target cwd.
This commit is contained in:
Peter Steinberger
2026-08-11 23:26:37 -07:00
committed by GitHub
parent 66fe424590
commit b080dd1e76
276 changed files with 1685 additions and 1663 deletions
+3 -7
View File
@@ -9,6 +9,7 @@ import { isPathInside } from "openclaw/plugin-sdk/security-runtime";
import {
asOptionalRecord,
normalizeLowercaseStringOrEmpty,
normalizeOptionalString,
normalizeStringEntries,
} from "openclaw/plugin-sdk/string-coerce-runtime";
import {
@@ -132,11 +133,6 @@ function resolveToolsAllow(params: { pluginToolsAllow: unknown; cfg?: OpenClawCo
);
}
function normalizePromptConfigText(value: unknown): string | undefined {
const text = typeof value === "string" ? value.trim() : "";
return text ? text : undefined;
}
function hasDeprecatedModelFallbackPolicy(pluginConfig: unknown): boolean {
const raw = asOptionalRecord(pluginConfig);
return raw ? Object.hasOwn(raw, "modelFallbackPolicy") : false;
@@ -239,8 +235,8 @@ function normalizePluginConfig(
fastMode: normalizeActiveMemoryFastMode(raw.fastMode),
promptStyle: resolvePromptStyle(raw.promptStyle, raw.queryMode),
toolsAllow: resolveToolsAllow({ pluginToolsAllow: raw.toolsAllow, cfg }),
promptOverride: normalizePromptConfigText(raw.promptOverride),
promptAppend: normalizePromptConfigText(raw.promptAppend),
promptOverride: normalizeOptionalString(raw.promptOverride),
promptAppend: normalizeOptionalString(raw.promptAppend),
timeoutMs: clampInt(
parseOptionalPositiveInt(raw.timeoutMs, DEFAULT_TIMEOUT_MS),
DEFAULT_TIMEOUT_MS,
@@ -1,5 +1,6 @@
import type { AgentMessage } from "openclaw/plugin-sdk/agent-harness-runtime";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
import { parseDateStringTimestampMs } from "openclaw/plugin-sdk/number-runtime";
import { withSessionTranscriptWriteLock } from "openclaw/plugin-sdk/session-transcript-runtime";
import { CLAUDE_CLI_BACKEND_ID } from "./cli-constants.js";
import type { ClaudeTranscriptItem } from "./session-catalog-transcript.js";
@@ -8,8 +9,7 @@ function importedClaudeMessage(
item: ClaudeTranscriptItem,
fallbackTimestamp: number,
): AgentMessage | undefined {
const parsedTimestamp = item.timestamp ? Date.parse(item.timestamp) : Number.NaN;
const timestamp = Number.isFinite(parsedTimestamp) ? parsedTimestamp : fallbackTimestamp;
const timestamp = parseDateStringTimestampMs(item.timestamp) ?? fallbackTimestamp;
const importedText = item.text?.trim();
if (!importedText && item.type === "reasoning") {
return undefined;
+1 -4
View File
@@ -14,6 +14,7 @@ import type {
SessionCatalogTranscriptItem,
} from "openclaw/plugin-sdk/session-catalog";
import {
asPositiveSafeInteger as pullRequestNumber,
isRecord,
normalizeBoundedOptionalString as readBoundedString,
} from "openclaw/plugin-sdk/string-coerce-runtime";
@@ -241,10 +242,6 @@ function pullRequestState(value: unknown): SessionCatalogPullRequestSummary["sta
}
}
function pullRequestNumber(value: unknown): number | undefined {
return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : undefined;
}
// Desktop retains historical PRs in order and marks hidden ones as dismissed;
// the top-level pair identifies the current PR whose state labels the row.
function desktopPullRequestSummary(
@@ -7,7 +7,7 @@ import {
type SessionUpstreamActivity,
type SessionUpstreamProbe,
} from "openclaw/plugin-sdk/session-catalog";
import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
import { asSafeIntegerInRange, isRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
import type { ClaudeTranscriptItem } from "./session-catalog-transcript.js";
const MAX_CLAUDE_UPSTREAM_SCAN_BYTES = 1024 * 1024;
@@ -119,7 +119,7 @@ function readMarkerOffset(probe: SessionUpstreamProbe): number | undefined {
return undefined;
}
const offset = probe.marker.offset ?? probe.marker.size;
return Number.isSafeInteger(offset) && (offset as number) >= 0 ? (offset as number) : undefined;
return asSafeIntegerInRange(offset, { min: 0 });
}
async function checkClaudeSessionUpstreamActivity(
+2 -1
View File
@@ -11,6 +11,7 @@ import {
parseStrictInteger,
resolveTimerTimeoutMs,
} from "openclaw/plugin-sdk/number-runtime";
import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
import type { BrowserActRequest } from "./client-actions.types.js";
import { DEFAULT_BROWSER_ACTION_TIMEOUT_MS } from "./constants.js";
@@ -110,7 +111,7 @@ function addNavigationGraceMs(durationMs: number, count = 1): number {
}
function isActionObject(value: unknown): value is BrowserActRequest {
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
return isRecord(value);
}
function resolveLeafExecutionBudgetMs(
+3 -3
View File
@@ -8,6 +8,7 @@ import {
type StableChannelIngressIdentityParams,
} from "openclaw/plugin-sdk/channel-ingress-runtime";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
import { parseDateStringTimestampMs } from "openclaw/plugin-sdk/number-runtime";
import {
normalizeAgentId,
type ResolvedAgentRoute,
@@ -249,6 +250,7 @@ export async function resolveClickClackInboundAccess(params: {
preparedRoute,
};
}
const botLoopNowMs = parseDateStringTimestampMs(params.message.created_at);
const botLoopProtection =
isBotAuthor && params.message.author_id !== params.account.botUserId && params.account.botUserId
? {
@@ -263,9 +265,7 @@ export async function resolveClickClackInboundAccess(params: {
senderId: params.message.author_id,
receiverId: params.account.botUserId,
eventId: params.message.id,
...(Number.isFinite(Date.parse(params.message.created_at))
? { nowMs: Date.parse(params.message.created_at) }
: {}),
...(botLoopNowMs !== undefined ? { nowMs: botLoopNowMs } : {}),
config: effectiveBotPolicy.botLoopProtection,
defaultsConfig: cfg.channels?.defaults?.botLoopProtection,
defaultEnabled: true,
@@ -2,10 +2,7 @@
* Codex-backed media understanding provider for bounded image description and
* structured extraction turns.
*/
import {
type JsonSchemaObject,
validateJsonSchemaValue,
} from "openclaw/plugin-sdk/json-schema-runtime";
import { validateJsonSchemaValue } from "openclaw/plugin-sdk/json-schema-runtime";
import type {
ImagesDescriptionRequest,
ImagesDescriptionResult,
@@ -13,6 +10,7 @@ import type {
StructuredExtractionRequest,
StructuredExtractionResult,
} from "openclaw/plugin-sdk/media-understanding";
import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
import {
runBoundedCodexAppServerTurn,
type CodexBoundedTurnOptions,
@@ -179,10 +177,6 @@ function buildStructuredExtractionPrompt(req: StructuredExtractionRequest): stri
.join("\n\n");
}
function isJsonSchemaObject(value: unknown): value is JsonSchemaObject {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function normalizeStructuredExtractionResult(params: {
text: string;
model: string;
@@ -201,7 +195,7 @@ function normalizeStructuredExtractionResult(params: {
} catch {
throw new Error("Codex structured extraction returned invalid JSON.");
}
if (isJsonSchemaObject(params.req.jsonSchema)) {
if (isRecord(params.req.jsonSchema)) {
const validation = validateJsonSchemaValue({
schema: params.req.jsonSchema,
cacheKey: "codex.media-understanding.extractStructured",
@@ -24,6 +24,7 @@ import type {
SessionTranscriptTargetParams,
TranscriptTurnAdmission,
} from "openclaw/plugin-sdk/session-transcript-runtime";
import { readNonBlankString as readNonEmptyString } from "openclaw/plugin-sdk/string-coerce-runtime";
import type { EmbeddedRunAttemptResult } from "./attempt-terminal.js";
import type { CodexDynamicToolFunctionSpec, CodexDynamicToolSpec, JsonValue } from "./protocol.js";
import { flattenCodexDynamicToolFunctions } from "./protocol.js";
@@ -502,10 +503,6 @@ function readPositiveNumber(value: unknown): number | undefined {
: undefined;
}
function readNonEmptyString(value: unknown): string | undefined {
return typeof value === "string" && value.trim().length > 0 ? value : undefined;
}
/**
* Builds OpenClaw-provided workspace prompt context for the current Codex turn.
*/
+3 -5
View File
@@ -10,6 +10,7 @@ import {
} from "openclaw/plugin-sdk/agent-harness-runtime";
import { resolveAgentDir, resolveDefaultAgentId } from "openclaw/plugin-sdk/agent-runtime";
import { createDedupeCache } from "openclaw/plugin-sdk/dedupe-runtime";
import { coerceErrorMessage } from "openclaw/plugin-sdk/error-runtime";
import { KeyedAsyncQueue } from "openclaw/plugin-sdk/keyed-async-queue";
import { asOptionalRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
import { isIncognitoSessionKey } from "../incognito-session.js";
@@ -963,13 +964,10 @@ function isCodexThreadNotFoundError(error: unknown): boolean {
// compaction.rs asserts message.contains("thread not found")). So the message
// is the authoritative positive signal here, not the generic code. This is a
// self-heal recovery gate, not user-facing classification.
return formatCompactionError(error).toLowerCase().includes("thread not found");
return coerceErrorMessage(error).toLowerCase().includes("thread not found");
}
function formatCompactionError(error: unknown): string {
if (error instanceof Error) {
return error.message;
}
return String(error);
return coerceErrorMessage(error);
}
/* oxlint-disable max-lines -- TODO: split this grandfathered oversized file. */
@@ -2,6 +2,8 @@ import { createHmac, randomBytes } from "node:crypto";
import { resolvePositiveTimerTimeoutMs } from "openclaw/plugin-sdk/number-runtime";
import { normalizeResolvedSecretInputString } from "openclaw/plugin-sdk/secret-input";
import {
asOptionalRecord as readRecord,
normalizeOptionalString as readNonEmptyString,
normalizeTrimmedStringList,
parseBooleanValue,
} from "openclaw/plugin-sdk/string-coerce-runtime";
@@ -12,11 +14,7 @@ const START_OPTIONS_KEY_SECRET_SYMBOL = Symbol.for("openclaw.codexAppServerStart
const START_OPTIONS_KEY_SECRET = getStartOptionsKeySecret();
const PLAIN_DECIMAL_NUMBER_RE = /^[+-]?(?:(?:\d+\.?\d*)|(?:\.\d+))$/;
export function readRecord(value: unknown): Record<string, unknown> | undefined {
return value && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: undefined;
}
export { readNonEmptyString, readRecord };
export function normalizeCodexServiceTier(value: unknown): CodexServiceTier | undefined {
if (typeof value !== "string") {
@@ -108,14 +106,6 @@ export function resolveArgs(configArgs: unknown, envArgs: string | undefined): s
return splitShellWords(envArgs ?? "");
}
export function readNonEmptyString(value: unknown): string | undefined {
if (typeof value !== "string") {
return undefined;
}
const trimmed = value.trim();
return trimmed || undefined;
}
export function hashSecretForKey(value: string | undefined, label: string): string | null {
if (!value) {
return null;
@@ -46,6 +46,7 @@ import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
import type { ImageContent, TextContent } from "openclaw/plugin-sdk/llm";
import { normalizeOpenAIToolSchemas } from "openclaw/plugin-sdk/provider-tools";
import {
asNonArrayRecord,
asOptionalRecord,
isRecord,
normalizeOptionalString,
@@ -557,7 +558,7 @@ export function createCodexDynamicToolBridge(params: {
handleToolCall: async (call, options) => {
const toolEntry = toolMap.get(call.tool);
if (!toolEntry) {
const executedArguments = jsonObjectToRecord(call.arguments);
const executedArguments = asNonArrayRecord(call.arguments);
const message = registeredToolNames.has(call.tool)
? `OpenClaw tool is not available for this turn: ${call.tool}`
: `Unknown OpenClaw tool: ${call.tool}`;
@@ -582,7 +583,7 @@ export function createCodexDynamicToolBridge(params: {
});
}
const { tool, name: toolName } = toolEntry;
const args = jsonObjectToRecord(call.arguments);
const args = asNonArrayRecord(call.arguments);
const startedAt = Date.now();
const signal = composeAbortSignals(params.signal, options?.signal);
let didStartExecution = false;
@@ -1530,12 +1531,6 @@ function convertToolContent(
},
];
}
function jsonObjectToRecord(value: JsonValue | undefined): Record<string, unknown> {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return {};
}
return value as Record<string, unknown>;
}
function readFirstString(record: Record<string, unknown>, keys: string[]): string | undefined {
for (const key of keys) {
const value = record[key];
@@ -2,7 +2,10 @@ import {
formatToolAggregate,
formatToolProgressOutput,
} from "openclaw/plugin-sdk/agent-harness-runtime";
import { readStringField as readString } from "openclaw/plugin-sdk/string-coerce-runtime";
import {
asNonArrayRecord,
readStringField as readString,
} from "openclaw/plugin-sdk/string-coerce-runtime";
import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
import { isJsonObject, type CodexThreadItem } from "./protocol.js";
@@ -104,10 +107,7 @@ export function toolOutputRawEchoSignature(
}
export function normalizeToolTranscriptArguments(value: unknown): Record<string, unknown> {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return {};
}
return value as Record<string, unknown>;
return asNonArrayRecord(value);
}
export function collectDynamicToolContentText(
@@ -1,14 +1,13 @@
import { normalizeUsage } from "openclaw/plugin-sdk/agent-harness-runtime";
import {
asFiniteNumber,
asSafeIntegerInRange,
readStringField as readString,
} from "openclaw/plugin-sdk/string-coerce-runtime";
import { readNonNegativeInteger } from "./event-projector-values.js";
import { isJsonObject, type JsonObject } from "./protocol.js";
function readTokenCount(record: JsonObject, key: string): number | undefined {
const value = readNonNegativeInteger(record, key);
return value !== undefined && Number.isSafeInteger(value) ? value : undefined;
return asSafeIntegerInRange(record[key], { min: 0 });
}
function readCodexThreadTokenUsage(params: JsonObject): ReturnType<typeof normalizeUsage> {
@@ -1,15 +1,14 @@
import { asFiniteNumber, readStringField } from "openclaw/plugin-sdk/string-coerce-runtime";
import {
asFiniteNumber,
normalizeOptionalString,
readStringField,
} from "openclaw/plugin-sdk/string-coerce-runtime";
import { isJsonObject, type CodexThreadItem, type JsonObject, type JsonValue } from "./protocol.js";
export function normalizeNonEmptyString(value: unknown): string | undefined {
if (typeof value !== "string") {
return undefined;
}
return value.trim() || undefined;
}
export { normalizeOptionalString as normalizeNonEmptyString };
export function readNonEmptyString(record: JsonObject, key: string): string | undefined {
return normalizeNonEmptyString(record[key]);
return normalizeOptionalString(record[key]);
}
export function readNonEmptyStringArray(record: JsonObject, key: string): string[] {
@@ -19,7 +18,7 @@ export function readNonEmptyStringArray(record: JsonObject, key: string): string
}
const entries: string[] = [];
for (const entry of value) {
const normalized = normalizeNonEmptyString(entry);
const normalized = normalizeOptionalString(entry);
if (normalized) {
entries.push(normalized);
}
+10 -18
View File
@@ -2,7 +2,7 @@
* Lists and normalizes models exposed by the Codex app-server `model/list`
* endpoint, including pagination and shared-client lease handling.
*/
import { uniqueStrings } from "openclaw/plugin-sdk/string-coerce-runtime";
import { normalizeOptionalString, uniqueStrings } from "openclaw/plugin-sdk/string-coerce-runtime";
import type {
CodexAppServerAuthRequirement,
resolveCodexAppServerAuthProfileIdForAgent,
@@ -145,8 +145,8 @@ export function readModelListResult(value: unknown): CodexAppServerModelListResu
}
function readCodexModel(value: CodexModel): CodexAppServerModel {
const id = readNonEmptyString(value.id);
const model = readNonEmptyString(value.model);
const id = normalizeOptionalString(value.id);
const model = normalizeOptionalString(value.model);
if (!id || !model) {
throw new Error(
"Invalid Codex app-server model/list response: model id and name must be non-empty strings",
@@ -155,37 +155,29 @@ function readCodexModel(value: CodexModel): CodexAppServerModel {
return {
id,
model,
...(readNonEmptyString(value.displayName)
? { displayName: readNonEmptyString(value.displayName) }
...(normalizeOptionalString(value.displayName)
? { displayName: normalizeOptionalString(value.displayName) }
: {}),
...(readNonEmptyString(value.description)
? { description: readNonEmptyString(value.description) }
...(normalizeOptionalString(value.description)
? { description: normalizeOptionalString(value.description) }
: {}),
hidden: value.hidden,
isDefault: value.isDefault,
inputModalities: value.inputModalities,
supportedReasoningEfforts: readReasoningEfforts(value.supportedReasoningEfforts),
...(readNonEmptyString(value.defaultReasoningEffort)
? { defaultReasoningEffort: readNonEmptyString(value.defaultReasoningEffort) }
...(normalizeOptionalString(value.defaultReasoningEffort)
? { defaultReasoningEffort: normalizeOptionalString(value.defaultReasoningEffort) }
: {}),
};
}
function readReasoningEfforts(value: CodexReasoningEffortOption[]): string[] {
const efforts = value
.map((entry) => readNonEmptyString(entry.reasoningEffort))
.map((entry) => normalizeOptionalString(entry.reasoningEffort))
.filter((entry): entry is string => entry !== undefined);
return uniqueStrings(efforts);
}
function readNonEmptyString(value: unknown): string | undefined {
if (typeof value !== "string") {
return undefined;
}
const trimmed = value.trim();
return trimmed || undefined;
}
function normalizeMaxPages(value: unknown): number {
return typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.floor(value) : 20;
}
@@ -715,17 +715,11 @@ function mergeJsonObjects(left: JsonObject, right: JsonObject): JsonObject {
for (const [key, value] of Object.entries(right)) {
const existing = merged[key];
merged[key] =
isPlainJsonObject(existing) && isPlainJsonObject(value)
? mergeJsonObjects(existing, value)
: value;
isJsonObject(existing) && isJsonObject(value) ? mergeJsonObjects(existing, value) : value;
}
return merged;
}
function isPlainJsonObject(value: JsonValue | undefined): value is JsonObject {
return Boolean(value && typeof value === "object" && !Array.isArray(value));
}
function fingerprintJson(value: JsonValue): string {
return crypto.createHash("sha256").update(stableStringify(value)).digest("hex");
}
+2 -1
View File
@@ -1,3 +1,4 @@
import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
import type { CodexCommandExecParams, CodexCommandExecResponse } from "./command-exec-protocol.js";
import type {
CodexAppInfo,
@@ -707,7 +708,7 @@ type CodexAppServerRequestResultMap = {
};
export function isJsonObject(value: unknown): value is JsonObject {
return Boolean(value && typeof value === "object" && !Array.isArray(value));
return isRecord(value);
}
export function isRpcResponse(message: RpcMessage): message is RpcResponse {
+1 -5
View File
@@ -1,4 +1,4 @@
import type { JsonValue } from "./protocol.js";
import { isJsonObject, type JsonValue } from "./protocol.js";
/** RPC error wrapper that preserves app-server error code and data. */
export class CodexAppServerRpcError extends Error {
@@ -36,7 +36,3 @@ function readCodexAppServerRpcReloginDetail(data: JsonValue | undefined): string
const detail = typeof nested.detail === "string" ? nested.detail.trim() : "";
return isRelogin && detail ? detail : undefined;
}
function isJsonObject(value: unknown): value is { [key: string]: JsonValue } {
return Boolean(value && typeof value === "object" && !Array.isArray(value));
}
@@ -1,6 +1,6 @@
import { Buffer } from "node:buffer";
import type { AgentMessage } from "openclaw/plugin-sdk/agent-harness-runtime";
import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
import { isRecord, normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
import type { JsonValue } from "./protocol.js";
import { readUpstreamUserText } from "./upstream-prompt-provenance.js";
@@ -18,10 +18,6 @@ type ProjectedMessageGroup = {
bytes: number;
};
function readNonEmptyString(value: unknown): string | undefined {
return typeof value === "string" ? value.trim() || undefined : undefined;
}
function readBoundedText(
value: unknown,
label: string,
@@ -49,7 +45,7 @@ function responseItemBytes(item: JsonValue): number {
}
function requireCallId(value: unknown): string {
const callId = readNonEmptyString(value);
const callId = normalizeOptionalString(value);
if (!callId || callId.length > 256) {
throw new Error("Codex settled-turn projection found an invalid tool call id");
}
@@ -57,7 +53,7 @@ function requireCallId(value: unknown): string {
}
function requireToolName(value: unknown): string {
const name = readNonEmptyString(value);
const name = normalizeOptionalString(value);
if (!name || !TOOL_NAME_PATTERN.test(name)) {
throw new Error("Codex settled-turn projection found an invalid tool name");
}
@@ -207,7 +203,7 @@ function projectToolResult(message: Record<string, unknown>): {
throw new Error("Codex settled-turn projection found malformed tool result content");
}
if (value.type === "image") {
const mimeType = readNonEmptyString(value.mimeType) ?? "unknown type";
const mimeType = normalizeOptionalString(value.mimeType) ?? "unknown type";
// The finalizer selects by text capability. Preserve image evidence as
// metadata without embedding an executable or oversized multimodal payload.
parts.push(`[Image tool result: ${mimeType}]`);
@@ -2,6 +2,7 @@ import {
isActiveHarnessContextEngine,
type EmbeddedRunAttemptParamsV2 as EmbeddedRunAttemptParams,
} from "openclaw/plugin-sdk/agent-harness-runtime";
import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
import {
resolveCodexContextEngineProjectionMaxChars,
resolveCodexContextEngineProjectionReserveTokens,
@@ -88,16 +89,12 @@ function areContextEngineProjectionBindingsCompatible(
}
function resolveContextEngineCitationsMode(config: unknown): JsonValue | undefined {
const rootConfig = isUnknownRecord(config) ? config : undefined;
const memoryConfig = isUnknownRecord(rootConfig?.memory) ? rootConfig.memory : undefined;
const rootConfig = isRecord(config) ? config : undefined;
const memoryConfig = isRecord(rootConfig?.memory) ? rootConfig.memory : undefined;
const citations = memoryConfig?.citations;
return isJsonConfigValue(citations) ? citations : undefined;
}
function isUnknownRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value && typeof value === "object" && !Array.isArray(value));
}
function isJsonConfigValue(value: unknown): value is JsonValue {
if (value === null || typeof value === "string" || typeof value === "boolean") {
return true;
@@ -108,5 +105,5 @@ function isJsonConfigValue(value: unknown): value is JsonValue {
if (Array.isArray(value)) {
return value.every(isJsonConfigValue);
}
return isUnknownRecord(value) && Object.values(value).every(isJsonConfigValue);
return isRecord(value) && Object.values(value).every(isJsonConfigValue);
}
@@ -8,7 +8,7 @@ import {
deleteSessionUpstreamLink,
upsertSessionUpstreamLink,
} from "openclaw/plugin-sdk/session-catalog";
import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
import { isRecord, normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
import { isIncognitoSessionKey } from "../incognito-session.js";
import type { CodexSessionCatalogControl } from "../session-catalog-types.js";
import { codexLastTerminalTurnId, codexUpstreamBaseline } from "../session-upstream-marker.js";
@@ -32,7 +32,7 @@ function readConnectionFingerprint(ref: unknown): string | undefined {
}
function normalizeTurnId(value: unknown): string | undefined {
return typeof value === "string" && value.trim() ? value.trim() : undefined;
return normalizeOptionalString(value);
}
export async function forkCodexUpstreamSession(
+6 -8
View File
@@ -12,7 +12,11 @@ import {
ModelSelectionLockedError,
} from "openclaw/plugin-sdk/model-session-runtime";
import type { OpenClawPluginToolContext } from "openclaw/plugin-sdk/plugin-entry";
import { asBoolean, asOptionalRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
import {
asBoolean,
asOptionalRecord,
asSafeIntegerInRange,
} from "openclaw/plugin-sdk/string-coerce-runtime";
import { Type } from "typebox";
import { resolveCodexBindingAppServerConnection } from "./app-server/binding-connection.js";
import { CODEX_CONTROL_METHODS } from "./app-server/capabilities.js";
@@ -112,12 +116,6 @@ type CodexThreadsToolOptions = {
request?: typeof codexControlRequest;
};
function readLimit(value: unknown): number | undefined {
return typeof value === "number" && Number.isInteger(value) && value >= 1 && value <= 100
? value
: undefined;
}
function resolveToolSession(
context: OpenClawPluginToolContext,
runtime: PluginRuntime,
@@ -291,7 +289,7 @@ export function createCodexThreadsTool(options: CodexThreadsToolOptions): AnyAge
CODEX_CONTROL_METHODS.listThreads,
{
archived: asBoolean(params.archived) ?? false,
limit: readLimit(params.limit) ?? 20,
limit: asSafeIntegerInRange(params.limit, { min: 1, max: 100 }) ?? 20,
modelProviders: [],
sortKey: "recency_at",
sortDirection: "desc",
@@ -1,10 +1,8 @@
import { readNonEmptyStringPreservingWhitespace as normalizeTurnId } from "openclaw/plugin-sdk/string-coerce-runtime";
import { describe, expect, it } from "vitest";
import type { CodexThread } from "./app-server/protocol.js";
import { codexUpstreamBaseline } from "./session-upstream-marker.js";
const normalizeTurnId = (value: unknown) =>
typeof value === "string" && value ? value : undefined;
describe("codexUpstreamBaseline", () => {
it("baselines an active adoption-time turn including its current user items", () => {
const thread = {
@@ -6,6 +6,7 @@ import {
wrapWebContent,
} from "openclaw/plugin-sdk/provider-web-search";
import type { WebSearchProviderPlugin } from "openclaw/plugin-sdk/provider-web-search-contract";
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
import {
runBoundedCodexAppServerTurn,
type CodexBoundedTurnOptions,
@@ -66,7 +67,7 @@ function summarizeCodexWebSearchItem(item: CodexThreadItem): Record<string, unkn
const actionType = readNonEmptyString(action, "type");
const queries = actionType === "search" ? readNonEmptyStringArray(action, "queries") : [];
const query =
normalizeNonEmptyString(item.query) ??
normalizeOptionalString(item.query) ??
(actionType === "search" ? readNonEmptyString(action, "query") : undefined) ??
queries[0];
const url = readNonEmptyString(action, "url");
@@ -81,7 +82,7 @@ function summarizeCodexWebSearchItem(item: CodexThreadItem): Record<string, unkn
}
function readNonEmptyString(record: JsonObject | undefined, key: string): string | undefined {
return record ? normalizeNonEmptyString(record[key]) : undefined;
return record ? normalizeOptionalString(record[key]) : undefined;
}
function readNonEmptyStringArray(record: JsonObject | undefined, key: string): string[] {
@@ -90,11 +91,7 @@ function readNonEmptyStringArray(record: JsonObject | undefined, key: string): s
return [];
}
return value.flatMap((entry) => {
const normalized = normalizeNonEmptyString(entry);
const normalized = normalizeOptionalString(entry);
return normalized ? [normalized] : [];
});
}
function normalizeNonEmptyString(value: unknown): string | undefined {
return typeof value === "string" ? value.trim() || undefined : undefined;
}
+3 -5
View File
@@ -20,6 +20,8 @@
// - `src/agents/pi-embedded-runner/run/types.ts` —
// `AgentHarnessAttemptResult.replayMetadata` field requirement.
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
type ReplayDecision =
| {
readonly action: "resume";
@@ -38,11 +40,7 @@ interface ReplayShimInput {
}
function normalizeSdkSessionId(value: unknown): string | undefined {
if (typeof value !== "string") {
return undefined;
}
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : undefined;
return normalizeOptionalString(value);
}
/**
@@ -4,6 +4,7 @@ import {
normalizeDiagnosticValue,
normalizeDiagnosticLane,
} from "openclaw/plugin-sdk/diagnostic-runtime";
import { asNonNegativeFiniteNumber as numericValue } from "openclaw/plugin-sdk/number-runtime";
import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
import type {
DiagnosticEventMetadata,
@@ -56,10 +57,6 @@ const RATIO_BUCKETS = [0.01, 0.05, 0.1, 0.25, 0.5, 0.75, 1, 2, 4, 8, 16];
const MAX_PROMETHEUS_SERIES = 2048;
const DROPPED_SERIES_COUNTER_NAME = "openclaw_prometheus_series_dropped_total";
function numericValue(value: number | undefined): number | undefined {
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
}
function seconds(ms: number | undefined): number | undefined {
const value = numericValue(ms);
return value === undefined ? undefined : value / 1000;
@@ -11,6 +11,7 @@ import {
} from "openclaw/plugin-sdk/proxy-capture";
import { danger, warn } from "openclaw/plugin-sdk/runtime-env";
import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env";
import { asFiniteNumber } from "openclaw/plugin-sdk/string-coerce-runtime";
import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
import * as ws from "ws";
import * as discordGateway from "../internal/gateway.js";
@@ -78,8 +79,7 @@ function readStringProperty(value: object, key: string): string | undefined {
}
function readNumberProperty(value: object, key: string): number | undefined {
const property = (value as Record<string, unknown>)[key];
return typeof property === "number" && Number.isFinite(property) ? property : undefined;
return asFiniteNumber((value as Record<string, unknown>)[key]);
}
function describeDiscordGatewayTransportError(error: Error): DiscordGatewayTransportErrorDetails {
@@ -33,17 +33,6 @@ vi.mock("openclaw/plugin-sdk/runtime-env", () => ({
danger: (value: string) => value,
}));
vi.mock("openclaw/plugin-sdk/string-coerce-runtime", () => {
const normalizeMockOptionalString = (value: string | null | undefined) => {
if (typeof value !== "string") {
return undefined;
}
const normalized = value.trim();
return normalized.length > 0 ? normalized : undefined;
};
return { normalizeOptionalString: normalizeMockOptionalString };
});
vi.mock("../proxy-request-client.js", () => ({
DISCORD_REST_TIMEOUT_MS: 15_000,
createDiscordRequestClient: vi.fn(() => ({
+4 -9
View File
@@ -1,5 +1,8 @@
// Feishu plugin module implements conversation id behavior.
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
import {
normalizeLowercaseStringOrEmpty,
normalizeOptionalString as normalizeText,
} from "openclaw/plugin-sdk/string-coerce-runtime";
export type FeishuGroupSessionScope =
| "group"
@@ -26,14 +29,6 @@ export function resolveConfiguredFeishuGroupSessionScope(params: {
);
}
function normalizeText(value: unknown): string | undefined {
if (typeof value !== "string") {
return undefined;
}
const trimmed = value.trim();
return trimmed || undefined;
}
export function buildFeishuConversationId(params: {
chatId: string;
scope: FeishuGroupSessionScope;
+2 -1
View File
@@ -3,6 +3,7 @@ import crypto from "node:crypto";
import * as http from "node:http";
import * as Lark from "@larksuiteoapi/node-sdk";
import { channelBlockedPatch, channelReadyPatch } from "openclaw/plugin-sdk/gateway-runtime";
import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
import { waitForAbortableDelay } from "./async.js";
import { createFeishuWSClient } from "./client.js";
@@ -56,7 +57,7 @@ const FEISHU_WS_AUTORECONNECT_DISABLED_ERROR =
"WebSocket connect failed and autoReconnect is disabled";
function isFeishuWebhookPayload(value: unknown): value is Record<string, unknown> {
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
return isRecord(value);
}
const BLOCKED_FEISHU_WEBHOOK_PAYLOAD_KEYS = new Set([
@@ -50,6 +50,7 @@ import path from "node:path";
import { minimatch } from "minimatch";
import { mutateConfigFile } from "openclaw/plugin-sdk/config-mutation";
import { getRuntimeConfig } from "openclaw/plugin-sdk/runtime-config-snapshot";
import { asNullableRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
export type FilePolicyKind = "read" | "write";
type FilePolicyAskMode = "off" | "on-miss" | "always";
@@ -85,10 +86,7 @@ type NodeFilePolicyConfig = {
type FilePolicyConfig = Record<string, NodeFilePolicyConfig>;
function asFilePolicyConfig(value: unknown): FilePolicyConfig | null {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return null;
}
return value as FilePolicyConfig;
return asNullableRecord(value) as FilePolicyConfig | null;
}
function readFilePolicyConfigFromPluginConfig(pluginConfig: unknown): FilePolicyConfig | null {
@@ -15,7 +15,11 @@ import {
resolveSpeechProviderApiKey,
trimToUndefined,
} from "openclaw/plugin-sdk/speech-core";
import { asFiniteNumberInRange, asOptionalRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
import {
asFiniteNumberInRange,
asOptionalRecord,
parseBooleanValue,
} from "openclaw/plugin-sdk/string-coerce-runtime";
import {
FISH_AUDIO_STREAM_MAX_BYTES,
type FishAudioFormat,
@@ -207,12 +211,9 @@ function parseDirectiveToken(ctx: SpeechDirectiveTokenParseContext) {
if (!ctx.policy.allowNormalization) {
return { handled: true };
}
const value = ctx.value.trim().toLowerCase();
if (["true", "1", "yes", "on"].includes(value)) {
return { handled: true, overrides: { ...ctx.currentOverrides, normalize: true } };
}
if (["false", "0", "no", "off"].includes(value)) {
return { handled: true, overrides: { ...ctx.currentOverrides, normalize: false } };
const normalize = parseBooleanValue(ctx.value);
if (normalize !== undefined) {
return { handled: true, overrides: { ...ctx.currentOverrides, normalize } };
}
return { handled: true, warnings: [`invalid Fish Audio normalize "${ctx.value}"`] };
}
+9 -7
View File
@@ -7,13 +7,15 @@ const coerceSecretRefMock = vi.hoisted(() => vi.fn());
const resolveConfiguredSecretInputWithFallbackMock = vi.hoisted(() => vi.fn());
const resolveRequiredConfiguredSecretRefInputStringMock = vi.hoisted(() => vi.fn());
vi.mock("openclaw/plugin-sdk/provider-auth", () => ({
coerceSecretRef: coerceSecretRefMock,
ensureAuthProfileStore: ensureAuthProfileStoreMock,
listProfilesForProvider: listProfilesForProviderMock,
normalizeOptionalSecretInput: (value: unknown) =>
typeof value === "string" && value.trim() ? value.trim() : undefined,
}));
vi.mock("openclaw/plugin-sdk/provider-auth", async () => {
const { normalizeOptionalString } = await import("openclaw/plugin-sdk/string-coerce-runtime");
return {
coerceSecretRef: coerceSecretRefMock,
ensureAuthProfileStore: ensureAuthProfileStoreMock,
listProfilesForProvider: listProfilesForProviderMock,
normalizeOptionalSecretInput: normalizeOptionalString,
};
});
vi.mock("openclaw/plugin-sdk/secret-input-runtime", () => ({
resolveConfiguredSecretInputWithFallback: resolveConfiguredSecretInputWithFallbackMock,
+5 -4
View File
@@ -6,7 +6,10 @@ import { normalizeAccountId, type OpenClawConfig } from "openclaw/plugin-sdk/acc
// Imessage plugin module implements accounts behavior.
import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
import { resolveAccountEntry } from "openclaw/plugin-sdk/routing";
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
import {
asOptionalRecord,
normalizeOptionalString,
} from "openclaw/plugin-sdk/string-coerce-runtime";
import type { IMessageAccountConfig } from "./account-types.js";
import {
expandIMessageUserPath,
@@ -45,9 +48,7 @@ function resolveIMessageAccountConfig(
type IMessageStreamingConfig = NonNullable<IMessageAccountConfig["streaming"]>;
function asStreamingConfigObject(value: unknown): IMessageStreamingConfig | undefined {
return value && typeof value === "object" && !Array.isArray(value)
? (value as IMessageStreamingConfig)
: undefined;
return asOptionalRecord(value) as IMessageStreamingConfig | undefined;
}
function mergeIMessageStreamingConfig(
+1 -4
View File
@@ -4,6 +4,7 @@ import {
parseStrictInteger,
resolveExpiresAtMsFromDurationMs,
} from "openclaw/plugin-sdk/number-runtime";
import { normalizeOptionalString as stringFromUnknown } from "openclaw/plugin-sdk/string-coerce-runtime";
import type { IMessageActionTransportOptions } from "./actions-rpc.js";
import { normalizeDirectChatIdentifier } from "./chat-context.js";
import { createIMessageRpcClient } from "./client.js";
@@ -41,10 +42,6 @@ function numberFromUnknown(value: unknown): number | undefined {
return typeof value === "number" && Number.isFinite(value) ? value : parseStrictInteger(value);
}
function stringFromUnknown(value: unknown): string | undefined {
return typeof value === "string" && value.trim() ? value.trim() : undefined;
}
function chatListCacheKey(options: IMessageActionTransportOptions): string {
return `${options.cliPath}\0${options.dbPath ?? ""}\0${options.remoteHost ?? ""}`;
}
@@ -2,6 +2,7 @@
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
import {
asDateTimestampMs,
asPositiveFiniteNumber,
resolveExpiresAtMsFromDurationMs,
} from "openclaw/plugin-sdk/number-runtime";
import type { IMessageApprovalGatewayRuntime } from "./approval-gateway-types.js";
@@ -38,7 +39,7 @@ type HistoryMessage = IMessagePayload & {
};
function normalizeChatId(value: unknown): number | null {
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : null;
return asPositiveFiniteNumber(value) ?? null;
}
function listTargetChatIds(
@@ -3,6 +3,7 @@ import {
formatInboundEnvelope,
type resolveEnvelopeFormatOptions,
} from "openclaw/plugin-sdk/channel-inbound";
import { parseDateStringTimestampMs } from "openclaw/plugin-sdk/number-runtime";
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
import type { IMessageRpcClient } from "../client.js";
import { normalizeIMessageHandle } from "../targets.js";
@@ -87,8 +88,7 @@ function historyEntryFromMessage(message: IMessagePayload, fallbackSender: strin
if (!body) {
return null;
}
const timestamp =
typeof message.created_at === "string" ? Date.parse(message.created_at) : Number.NaN;
const timestamp = parseDateStringTimestampMs(message.created_at);
return {
sender:
message.is_from_me === true
@@ -96,7 +96,7 @@ function historyEntryFromMessage(message: IMessagePayload, fallbackSender: strin
: normalizeIMessageHandle(normalizeOptionalString(message.sender) ?? fallbackSender) ||
fallbackSender,
body,
...(Number.isFinite(timestamp) ? { timestamp } : {}),
...(timestamp !== undefined ? { timestamp } : {}),
};
}
+2 -2
View File
@@ -10,6 +10,7 @@ import { isRecord } from "openclaw/plugin-sdk/channel-secret-basic-runtime";
import { collectErrorGraphCandidates, formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
import { KeyedAsyncQueue } from "openclaw/plugin-sdk/keyed-async-queue";
import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env";
import { asSafeIntegerInRange } from "openclaw/plugin-sdk/string-coerce-runtime";
import { getIMessageRuntime } from "../runtime.js";
import { parseIMessageNotification } from "./parse-notification.js";
import type { IMessagePayload } from "./types.js";
@@ -62,8 +63,7 @@ function rawMessageRecord(raw: unknown): Record<string, unknown> | null {
}
function rawRowid(raw: unknown): number | null {
const rowid = rawMessageRecord(raw)?.id;
return typeof rowid === "number" && Number.isSafeInteger(rowid) && rowid >= 0 ? rowid : null;
return asSafeIntegerInRange(rawMessageRecord(raw)?.id, { min: 0 }) ?? null;
}
/** Read only stable transport metadata; payload normalization waits for dispatch. */
+1 -4
View File
@@ -22,6 +22,7 @@ import {
import { requireRuntimeConfig } from "openclaw/plugin-sdk/plugin-config-runtime";
import { sleep as delay } from "openclaw/plugin-sdk/runtime-env";
import { openNodeSqliteDatabase } from "openclaw/plugin-sdk/sqlite-runtime";
import { normalizeOptionalString as stringValue } from "openclaw/plugin-sdk/string-coerce-runtime";
import { resolvePreferredOpenClawTmpDir, withTempWorkspace } from "openclaw/plugin-sdk/temp-path";
import { convertMarkdownTables } from "openclaw/plugin-sdk/text-chunking";
import { stripInlineDirectiveTagsForDelivery } from "openclaw/plugin-sdk/text-chunking";
@@ -518,10 +519,6 @@ async function runIMessageCliJson(
});
}
function stringValue(value: unknown): string | undefined {
return typeof value === "string" && value.trim() ? value.trim() : undefined;
}
function resultService(value: unknown): Exclude<IMessageService, "auto"> | undefined {
const normalized = stringValue(value)?.toLowerCase();
return normalized === "imessage" || normalized === "sms" ? normalized : undefined;
+2 -2
View File
@@ -6,7 +6,7 @@ import type { ChannelLegacyStateMigrationPlan } from "openclaw/plugin-sdk/channe
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
import { fileExists } from "openclaw/plugin-sdk/security-runtime";
import { resolveStateDir } from "openclaw/plugin-sdk/state-paths";
import { uniqueStrings } from "openclaw/plugin-sdk/string-coerce-runtime";
import { asFiniteNumber, uniqueStrings } from "openclaw/plugin-sdk/string-coerce-runtime";
import {
listIMessageAccountIds,
resolveDefaultIMessageAccountId,
@@ -131,7 +131,7 @@ function readReplyCounterValue(value: unknown): number | null {
return null;
}
const counter = (value as { counter?: unknown }).counter;
return typeof counter === "number" && Number.isFinite(counter) ? counter : null;
return asFiniteNumber(counter) ?? null;
}
function shouldReplaceReplyCounter(existingValue: unknown, incomingValue: unknown): boolean {
+5 -4
View File
@@ -5,6 +5,7 @@ import { randomUUID } from "node:crypto";
import { chmod, mkdir, readFile, rm, writeFile } from "node:fs/promises";
import path from "node:path";
import { runExec } from "openclaw/plugin-sdk/process-runtime";
import { asFiniteNumber } from "openclaw/plugin-sdk/string-coerce-runtime";
import { resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk/temp-path";
type LogbookSnapshotParams = {
@@ -22,11 +23,11 @@ function readParams(value: unknown): LogbookSnapshotParams {
return {};
}
const record = value as Record<string, unknown>;
const num = (key: string) => {
const candidate = record[key];
return typeof candidate === "number" && Number.isFinite(candidate) ? candidate : undefined;
return {
screenIndex: asFiniteNumber(record.screenIndex),
maxWidth: asFiniteNumber(record.maxWidth),
quality: asFiniteNumber(record.quality),
};
return { screenIndex: num("screenIndex"), maxWidth: num("maxWidth"), quality: num("quality") };
}
export async function handleLogbookSnapshot(rawParams: unknown): Promise<LogbookSnapshotPayload> {
@@ -17,7 +17,7 @@ function readPackageJson(packageRoot) {
}
}
function normalizeLowercaseStringOrEmpty(value) {
function lowercaseStringOrEmptyWithoutTrim(value) {
return typeof value === "string" ? value.toLowerCase() : "";
}
@@ -29,7 +29,7 @@ function hasTrustedOpenClawRootIndicator(packageRoot, packageJson) {
const hasCliEntryExport = Object.hasOwn(packageExports, "./cli-entry");
const hasOpenClawBin =
(typeof packageJson?.bin === "string" &&
normalizeLowercaseStringOrEmpty(packageJson.bin).includes("openclaw")) ||
lowercaseStringOrEmptyWithoutTrim(packageJson.bin).includes("openclaw")) ||
(typeof packageJson?.bin === "object" &&
packageJson.bin !== null &&
typeof packageJson.bin.openclaw === "string");
@@ -1,4 +1,9 @@
// Mattermost plugin module implements gateway auth bypass behavior.
import {
asOptionalRecord,
normalizeOptionalString as readTrimmedString,
} from "openclaw/plugin-sdk/string-coerce-runtime";
const DEFAULT_SLASH_CALLBACK_PATH = "/api/channels/mattermost/command";
type MattermostSlashCommandConfigInput = {
@@ -14,10 +19,6 @@ type MattermostConfigInput = MattermostAccountConfigInput & {
accounts?: Record<string, unknown>;
};
function readTrimmedString(value: unknown): string | undefined {
return typeof value === "string" && value.trim() ? value.trim() : undefined;
}
function normalizeCallbackPath(value: unknown): string {
const trimmed = readTrimmedString(value);
if (!trimmed) {
@@ -27,9 +28,7 @@ function normalizeCallbackPath(value: unknown): string {
}
function readMattermostCommands(value: unknown): MattermostSlashCommandConfigInput | undefined {
return value && typeof value === "object" && !Array.isArray(value)
? (value as MattermostSlashCommandConfigInput)
: undefined;
return asOptionalRecord(value) as MattermostSlashCommandConfigInput | undefined;
}
function isMattermostBypassPath(path: string): boolean {
@@ -1,6 +1,7 @@
// Memory Core tests cover embeddings plugin behavior.
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
import type { EmbeddingProviderAdapter } from "openclaw/plugin-sdk/embedding-providers";
import { coerceErrorMessage } from "openclaw/plugin-sdk/error-runtime";
import type { MemoryEmbeddingProviderAdapter } from "openclaw/plugin-sdk/memory-core-host-engine-embeddings";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import {
@@ -105,7 +106,7 @@ function createMissingCredentialsAdapter(
id: "bedrock",
transport: "remote",
autoSelectPriority: 60,
formatSetupError: (err) => (err instanceof Error ? err.message : String(err)),
formatSetupError: coerceErrorMessage,
shouldContinueAutoSelection: (err) =>
err instanceof Error && err.message.includes("No API key found for provider"),
create: async () => {
@@ -1,4 +1,5 @@
// Msteams plugin module implements bot framework behavior.
import { coerceErrorMessage } from "openclaw/plugin-sdk/error-runtime";
import { parseMediaContentLength } from "openclaw/plugin-sdk/media-runtime";
import { readProviderJsonResponse } from "openclaw/plugin-sdk/provider-http";
import {
@@ -108,7 +109,7 @@ async function fetchBotFrameworkAttachmentInfo(params: {
});
} catch (err) {
params.logger?.warn?.("msteams botFramework attachmentInfo fetch failed", {
error: err instanceof Error ? err.message : String(err),
error: coerceErrorMessage(err),
});
return undefined;
}
@@ -126,7 +127,7 @@ async function fetchBotFrameworkAttachmentInfo(params: {
);
} catch (err) {
params.logger?.warn?.("msteams botFramework attachmentInfo parse failed", {
error: err instanceof Error ? err.message : String(err),
error: coerceErrorMessage(err),
});
return undefined;
}
@@ -168,7 +169,7 @@ async function saveBotFrameworkAttachmentView(params: {
});
} catch (err) {
params.logger?.warn?.("msteams botFramework attachmentView fetch failed", {
error: err instanceof Error ? err.message : String(err),
error: coerceErrorMessage(err),
});
return undefined;
}
@@ -185,7 +186,7 @@ async function saveBotFrameworkAttachmentView(params: {
} catch (err) {
await response.body?.cancel().catch(() => undefined);
params.logger?.warn?.("msteams botFramework attachmentView invalid content-length", {
error: err instanceof Error ? err.message : String(err),
error: coerceErrorMessage(err),
});
return undefined;
}
@@ -204,7 +205,7 @@ async function saveBotFrameworkAttachmentView(params: {
});
} catch (err) {
params.logger?.warn?.("msteams botFramework attachmentView save failed", {
error: err instanceof Error ? err.message : String(err),
error: coerceErrorMessage(err),
});
return undefined;
} finally {
@@ -256,7 +257,7 @@ async function downloadMSTeamsBotFrameworkAttachment(params: {
});
} catch (err) {
params.logger?.warn?.("msteams botFramework token acquisition failed", {
error: err instanceof Error ? err.message : String(err),
error: coerceErrorMessage(err),
});
return undefined;
}
@@ -401,7 +402,7 @@ export async function downloadMSTeamsBotFrameworkAttachments(params: {
} catch (err) {
media.push({ kind: "document", sourceId: attachmentId });
params.logger?.warn?.("msteams botFramework attachment download failed", {
error: err instanceof Error ? err.message : String(err),
error: coerceErrorMessage(err),
attachmentId,
});
}
@@ -1,4 +1,5 @@
// Msteams plugin module implements download behavior.
import { coerceErrorMessage } from "openclaw/plugin-sdk/error-runtime";
import {
normalizeLowercaseStringOrEmpty,
normalizeOptionalLowercaseString,
@@ -334,7 +335,7 @@ export async function downloadMSTeamsAttachments(params: {
} catch (err) {
out.push(withSourceId({ kind: candidate.mediaKind }, candidate.sourceId));
params.logger?.warn?.("msteams inline attachment decode failed", {
error: err instanceof Error ? err.message : String(err),
error: coerceErrorMessage(err),
});
}
continue;
@@ -370,7 +371,7 @@ export async function downloadMSTeamsAttachments(params: {
out.push(withSourceId(media, candidate.sourceId));
} catch (err) {
out.push(withSourceId({ kind: candidate.mediaKind }, candidate.sourceId));
const msg = err instanceof Error ? err.message : String(err);
const msg = coerceErrorMessage(err);
params.logger?.warn?.(
`msteams attachment download failed host=${safeHostForLog(candidate.url)} error=${msg}`,
);
@@ -4,8 +4,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
// Mock shared.js to avoid transitive runtime-api imports that pull in uninstalled packages.
vi.mock("./shared.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("./shared.js")>();
const isMockRecord = (value: unknown) =>
typeof value === "object" && value !== null && !Array.isArray(value);
const { isRecord } = await import("openclaw/plugin-sdk/string-coerce-runtime");
return {
...actual,
applyAuthorizationHeaderForUrl: vi.fn(),
@@ -13,7 +12,7 @@ vi.mock("./shared.js", async (importOriginal) => {
resolveMSTeamsMediaKind: vi.fn(({ contentType }: { contentType?: string }) =>
contentType?.startsWith("image/") ? "image" : "document",
),
isRecord: isMockRecord,
isRecord,
isUrlAllowed: vi.fn(() => true),
normalizeContentType: vi.fn((ct: string | null | undefined) => ct ?? undefined),
resolveMediaSsrfPolicy: vi.fn(() => undefined),
+9 -8
View File
@@ -1,4 +1,5 @@
// Msteams plugin module implements graph behavior.
import { coerceErrorMessage } from "openclaw/plugin-sdk/error-runtime";
import {
readProviderJsonArrayFieldResponse,
readProviderJsonResponse,
@@ -188,7 +189,7 @@ async function downloadGraphHostedContent(params: {
})) as { status: number; items: GraphHostedContent[] };
} catch (err) {
params.logger?.warn?.("msteams graph hostedContents fetch failed", {
error: err instanceof Error ? err.message : String(err),
error: coerceErrorMessage(err),
});
return { media: [], count: 0 };
}
@@ -240,7 +241,7 @@ async function downloadGraphHostedContent(params: {
} catch (err) {
out.push(createGraphHostedContentFact(item));
params.logger?.warn?.("msteams graph hostedContent value fetch failed", {
error: err instanceof Error ? err.message : String(err),
error: coerceErrorMessage(err),
});
continue;
}
@@ -324,10 +325,10 @@ export async function downloadMSTeamsGraphMedia(params: {
} catch (err) {
params.logger?.debug?.("graph media message parse failed", {
messageUrl,
error: err instanceof Error ? err.message : String(err),
error: coerceErrorMessage(err),
});
params.logger?.warn?.("msteams graph message parse failed", {
error: err instanceof Error ? err.message : String(err),
error: coerceErrorMessage(err),
messageUrl,
});
msgData = {};
@@ -349,10 +350,10 @@ export async function downloadMSTeamsGraphMedia(params: {
} catch (err) {
params.logger?.debug?.("graph media message fetch failed", {
messageUrl,
error: err instanceof Error ? err.message : String(err),
error: coerceErrorMessage(err),
});
params.logger?.warn?.("msteams graph message fetch failed", {
error: err instanceof Error ? err.message : String(err),
error: coerceErrorMessage(err),
});
}
@@ -423,7 +424,7 @@ export async function downloadMSTeamsGraphMedia(params: {
} catch (err) {
sharePointMedia.push(unavailableMedia);
params.logger?.warn?.("msteams SharePoint reference download failed", {
error: err instanceof Error ? err.message : String(err),
error: coerceErrorMessage(err),
name,
});
}
@@ -472,7 +473,7 @@ export async function downloadMSTeamsGraphMedia(params: {
});
} catch (err) {
params.logger?.warn?.("msteams graph attachment download failed", {
error: err instanceof Error ? err.message : String(err),
error: coerceErrorMessage(err),
messageUrl,
});
}
@@ -45,20 +45,22 @@ const keepHttpServerTaskAliveMock = vi.hoisted(() =>
}),
);
vi.mock("../runtime-api.js", () => ({
DEFAULT_WEBHOOK_MAX_BODY_BYTES: 1024 * 1024,
isDangerousNameMatchingEnabled,
normalizeSecretInputString: (value: unknown) =>
typeof value === "string" && value.trim() ? value.trim() : undefined,
hasConfiguredSecretInput: (value: unknown) =>
typeof value === "string" && value.trim().length > 0,
normalizeResolvedSecretInputString: (params: { value?: unknown }) =>
typeof params?.value === "string" && params.value.trim() ? params.value.trim() : undefined,
keepHttpServerTaskAlive: keepHttpServerTaskAliveMock,
mergeAllowlist: (params: { existing?: string[]; additions: string[] }) =>
Array.from(new Set([...(params.existing ?? []), ...params.additions])),
summarizeMapping: vi.fn(),
}));
vi.mock("../runtime-api.js", async () => {
const { normalizeOptionalString } = await import("openclaw/plugin-sdk/string-coerce-runtime");
return {
DEFAULT_WEBHOOK_MAX_BODY_BYTES: 1024 * 1024,
isDangerousNameMatchingEnabled,
normalizeSecretInputString: normalizeOptionalString,
hasConfiguredSecretInput: (value: unknown) =>
typeof value === "string" && value.trim().length > 0,
normalizeResolvedSecretInputString: (params: { value?: unknown }) =>
typeof params?.value === "string" && params.value.trim() ? params.value.trim() : undefined,
keepHttpServerTaskAlive: keepHttpServerTaskAliveMock,
mergeAllowlist: (params: { existing?: string[]; additions: string[] }) =>
Array.from(new Set([...(params.existing ?? []), ...params.additions])),
summarizeMapping: vi.fn(),
};
});
vi.mock("express", () => ({
default: () => {
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
import { once } from "node:events";
import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
import type { AddressInfo } from "node:net";
import { coerceErrorMessage } from "openclaw/plugin-sdk/error-runtime";
type MSTeamsQaOutboundActivity = {
activity: Record<string, unknown>;
@@ -74,7 +75,7 @@ export async function startMSTeamsQaBotFrameworkServer(options: ServerOptions) {
sendJson(response, 200, { id: activityId });
})().catch((error: unknown) => {
sendJson(response, 500, {
error: error instanceof Error ? error.message : String(error),
error: coerceErrorMessage(error),
});
});
});
+2 -1
View File
@@ -1,4 +1,5 @@
// Msteams plugin module implements sdk behavior.
import { coerceErrorMessage } from "openclaw/plugin-sdk/error-runtime";
import { createLazyRuntimeModule } from "openclaw/plugin-sdk/lazy-runtime";
import { readSecretFile } from "openclaw/plugin-sdk/secret-file";
import { normalizeBotFrameworkServiceUrl } from "./bot-framework-service-url.js";
@@ -333,7 +334,7 @@ async function createFederatedApp(
try {
privateKey = await readSecretFile(creds.certificatePath, "Microsoft Teams certificate");
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : String(err);
const msg = coerceErrorMessage(err);
throw new Error(`Failed to read certificate file at '${creds.certificatePath}': ${msg}`, {
cause: err,
});
+9 -7
View File
@@ -26,13 +26,15 @@ vi.mock("./oauth.token.js", () => ({
refreshMSTeamsDelegatedTokens: oauthTokenMocks.refreshMSTeamsDelegatedTokens,
}));
vi.mock("./secret-input.js", () => ({
normalizeSecretInputString: (v: unknown) =>
typeof v === "string" && v.trim() ? v.trim() : undefined,
normalizeResolvedSecretInputString: (opts: { value: unknown; path: string }) =>
typeof opts.value === "string" && opts.value.trim() ? opts.value.trim() : undefined,
hasConfiguredSecretInput: (v: unknown) => typeof v === "string" && v.trim().length > 0,
}));
vi.mock("./secret-input.js", async () => {
const { normalizeOptionalString } = await import("openclaw/plugin-sdk/string-coerce-runtime");
return {
normalizeSecretInputString: normalizeOptionalString,
normalizeResolvedSecretInputString: (opts: { value: unknown; path: string }) =>
typeof opts.value === "string" && opts.value.trim() ? opts.value.trim() : undefined,
hasConfiguredSecretInput: (v: unknown) => typeof v === "string" && v.trim().length > 0,
};
});
const ENV_KEYS = [
"MSTEAMS_APP_ID",
+4 -7
View File
@@ -6,6 +6,7 @@ import {
archiveLegacyStateSource,
type PluginDoctorStateMigration,
} from "openclaw/plugin-sdk/runtime-doctor-migrations";
import { asFiniteNumber } from "openclaw/plugin-sdk/string-coerce-runtime";
import { normalizeNostrStateAccountId } from "./src/state-account-id.js";
type NostrBusState = {
@@ -26,10 +27,6 @@ const BUS_STATE_NAMESPACE = "bus-state";
const PROFILE_STATE_NAMESPACE = "profile-state";
const MAX_NOSTR_STATE_ENTRIES = 256;
function finiteNumberOrNull(value: unknown): number | null {
return typeof value === "number" && Number.isFinite(value) ? value : null;
}
function parseBusState(value: unknown): NostrBusState | null {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return null;
@@ -40,8 +37,8 @@ function parseBusState(value: unknown): NostrBusState | null {
}
return {
version: 2,
lastProcessedAt: finiteNumberOrNull(parsed.lastProcessedAt),
gatewayStartedAt: finiteNumberOrNull(parsed.gatewayStartedAt),
lastProcessedAt: asFiniteNumber(parsed.lastProcessedAt) ?? null,
gatewayStartedAt: asFiniteNumber(parsed.gatewayStartedAt) ?? null,
recentEventIds:
parsed.version === 2 && Array.isArray(parsed.recentEventIds)
? parsed.recentEventIds.filter((entry): entry is string => typeof entry === "string")
@@ -68,7 +65,7 @@ function parseProfileState(value: unknown): NostrProfileState | null {
}
return {
version: 1,
lastPublishedAt: finiteNumberOrNull(parsed.lastPublishedAt),
lastPublishedAt: asFiniteNumber(parsed.lastPublishedAt) ?? null,
lastPublishedEventId:
typeof parsed.lastPublishedEventId === "string" ? parsed.lastPublishedEventId : null,
lastPublishResults:
@@ -1,3 +1,4 @@
import { coerceErrorMessage } from "openclaw/plugin-sdk/error-runtime";
// Ollama tests cover embedding provider plugin behavior.
import type { OpenClawConfig } from "openclaw/plugin-sdk/provider-auth";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
@@ -14,7 +15,7 @@ const { fetchConfiguredLocalOriginWithSsrFGuardMock } = vi.hoisted(() => ({
vi.mock("openclaw/plugin-sdk/ssrf-runtime", () => ({
fetchWithSsrFGuard: vi.fn(),
formatErrorMessage: (error: unknown) => (error instanceof Error ? error.message : String(error)),
formatErrorMessage: coerceErrorMessage,
ssrfPolicyFromHttpBaseUrlAllowedOrigin: (baseUrl: string) => {
const parsed = new URL(baseUrl);
return { allowedOrigins: [parsed.origin] };
+3 -7
View File
@@ -18,7 +18,7 @@ import {
readResponseTextLimited,
} from "openclaw/plugin-sdk/provider-http";
import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime";
import { asNullableRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
import { asFiniteNumber, asNullableRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
import { OLLAMA_DEFAULT_BASE_URL } from "./defaults.js";
import {
DEFAULT_INFERENCE_TIMEOUT_MS,
@@ -101,10 +101,6 @@ function durationMs(value: unknown): number | undefined {
return Math.round((value / 1_000_000) * 100) / 100;
}
function optionalNumber(value: unknown): number | undefined {
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
}
async function requestOllamaJson<T>(params: {
baseUrl: string;
path: string;
@@ -300,8 +296,8 @@ async function runOllamaNodeChat(params: {
`Ollama stopped after reaching maxTokens (${params.maxTokens}); retry with a larger maxTokens value`,
);
}
const promptTokens = optionalNumber(data.prompt_eval_count);
const completionTokens = optionalNumber(data.eval_count);
const promptTokens = asFiniteNumber(data.prompt_eval_count);
const completionTokens = asFiniteNumber(data.eval_count);
const loadMs = durationMs(data.load_duration);
const totalMs = durationMs(data.total_duration);
return {
+6 -2
View File
@@ -18,7 +18,11 @@ import { readResponseTextLimited } from "openclaw/plugin-sdk/provider-http";
import { createPlainTextToolCallCompatWrapper } from "openclaw/plugin-sdk/provider-stream-shared";
import { createSubsystemLogger } from "openclaw/plugin-sdk/runtime-env";
import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime";
import { isRecord, readStringValue } from "openclaw/plugin-sdk/string-coerce-runtime";
import {
isRecord,
normalizeOptionalString,
readStringValue,
} from "openclaw/plugin-sdk/string-coerce-runtime";
import { estimateStringChars, truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
import { OLLAMA_CLOUD_BASE_URL, OLLAMA_DEFAULT_BASE_URL } from "./defaults.js";
import { normalizeOllamaWireModelId } from "./model-id.js";
@@ -663,7 +667,7 @@ type OllamaAssistantMessageBuildOptions = OllamaToolCallNameOptions & {
};
function readOllamaToolCallId(value: unknown): string | undefined {
return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
return normalizeOptionalString(value);
}
function extractToolCalls(
+1 -4
View File
@@ -6,6 +6,7 @@ import OpenAI from "openai";
import type { ResolvedTtsConfig } from "openclaw/plugin-sdk/agent-runtime";
import { AuthStorage, ModelRegistry } from "openclaw/plugin-sdk/agent-sessions";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
import { coerceErrorMessage as formatLiveOpenAIError } from "openclaw/plugin-sdk/error-runtime";
import { encodePngRgba, fillPixel } from "openclaw/plugin-sdk/media-runtime";
import {
registerProviderPlugin,
@@ -81,10 +82,6 @@ function createReferencePng(): Buffer {
return encodePngRgba(buf, width, height);
}
function formatLiveOpenAIError(error: unknown): string {
return error instanceof Error ? error.message : String(error);
}
function resolveLiveOpenAISkipReason(error: unknown): string | null {
const message = formatLiveOpenAIError(error);
if (isTimeoutErrorMessage(message) || /timed out|operation was aborted/i.test(message)) {
+3 -6
View File
@@ -11,6 +11,7 @@ import type {
ProviderResponseModelEquivalenceContext,
ProviderResolveModelRoutesContext,
} from "openclaw/plugin-sdk/provider-model-types";
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
import {
classifyOpenAIBaseUrl,
isOpenAICodexBaseUrl,
@@ -44,11 +45,7 @@ type OpenAIResolveSingleModelRouteContext = Omit<
};
function normalizeOptionalRouteApi(value: ModelApi | null | undefined): ModelApi | undefined {
return typeof value === "string" && value.trim() ? (value.trim() as ModelApi) : undefined;
}
function normalizeOptionalRouteBaseUrl(value: unknown): string | undefined {
return typeof value === "string" && value.trim() ? value.trim() : undefined;
return normalizeOptionalString(value) as ModelApi | undefined;
}
/** Canonical logical id for OpenAI catalog projection. */
@@ -138,7 +135,7 @@ function firstRouteBaseUrl(...values: unknown[]): unknown {
}
function concreteBaseUrl(value: unknown, fallback: string): string {
return normalizeOptionalRouteBaseUrl(value) ?? fallback;
return normalizeOptionalString(value) ?? fallback;
}
function resolveOpenAIEnvironmentBaseUrl(
+2 -1
View File
@@ -1,5 +1,6 @@
// Openai tests cover speech provider plugin behavior.
import { createServer } from "node:http";
import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
import { afterEach, describe, expect, it, vi } from "vitest";
import { buildOpenAISpeechProvider } from "./speech-provider.js";
@@ -26,7 +27,7 @@ function isSpeechRequestBody(value: unknown): value is {
speed?: number;
response_format?: string;
} {
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
return isRecord(value);
}
function parseRequestBody(init: RequestInit | undefined): {
@@ -6,6 +6,7 @@ import type {
HealthRepairResult,
OpenClawConfig,
} from "openclaw/plugin-sdk/health";
import { uniqueStrings } from "openclaw/plugin-sdk/string-coerce-runtime";
import { CHECK_IDS, type POLICY_CHECK_IDS } from "./check-ids.js";
import { POLICY_FIX_METADATA_BY_CHECK_ID } from "./fix-metadata.js";
@@ -447,7 +448,3 @@ function ensureRecord(parent: ConfigRecord, key: string): ConfigRecord {
parent[key] = next;
return next;
}
function uniqueStrings(values: readonly string[]): readonly string[] {
return [...new Set(values)];
}
@@ -5,6 +5,7 @@ import type {
HealthRepairEffect,
HealthRepairResult,
} from "openclaw/plugin-sdk/health";
import { uniqueStrings } from "openclaw/plugin-sdk/string-coerce-runtime";
import { CHECK_IDS, type POLICY_CHECK_IDS } from "./check-ids.js";
import { POLICY_FIX_METADATA_BY_CHECK_ID } from "./fix-metadata.js";
@@ -121,10 +122,6 @@ function previewGatewayNodeDenyCommand(
];
}
function uniqueStrings(values: readonly string[]): readonly string[] {
return [...new Set(values)];
}
function uniqueEffects(values: readonly HealthRepairEffect[]): readonly HealthRepairEffect[] {
const seen = new Set<string>();
return values.filter((value) => {
+5 -14
View File
@@ -8,7 +8,7 @@ import {
} from "@openclaw/crabline";
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
import { parseStrictPositiveInteger } from "openclaw/plugin-sdk/number-runtime";
import { uniqueStrings } from "openclaw/plugin-sdk/string-coerce-runtime";
import { parseBooleanValue, uniqueStrings } from "openclaw/plugin-sdk/string-coerce-runtime";
import {
buildQaAgenticParityComparison,
buildQaRuntimeParityReport,
@@ -238,20 +238,11 @@ function parseQaModelThinkingOverrides(entries: readonly string[] | undefined) {
}
function parseQaBooleanModelOption(label: string, value: string) {
switch (value.trim().toLowerCase()) {
case "1":
case "on":
case "true":
case "yes":
return true;
case "0":
case "false":
case "no":
case "off":
return false;
default:
throw new Error(`${label} fast must be one of true, false, on, off, yes, no, 1, 0`);
const parsed = parseBooleanValue(value);
if (parsed === undefined) {
throw new Error(`${label} fast must be one of true, false, on, off, yes, no, 1, 0`);
}
return parsed;
}
function parseQaPositiveIntegerOption(label: string, value: number | undefined) {
+4 -5
View File
@@ -1,5 +1,8 @@
// Qa Lab plugin module implements coverage report behavior.
import { normalizeStringEntriesLower } from "openclaw/plugin-sdk/string-coerce-runtime";
import {
normalizeOptionalString as stringifyConfigValue,
normalizeStringEntriesLower,
} from "openclaw/plugin-sdk/string-coerce-runtime";
import type { QaSeedScenarioWithSource } from "./scenario-catalog.js";
import {
readQaScorecardTaxonomyReport,
@@ -134,10 +137,6 @@ function scenarioSearchText(scenario: QaSeedScenarioWithSource) {
);
}
function stringifyConfigValue(value: unknown) {
return typeof value === "string" && value.trim() ? value.trim() : undefined;
}
function summarizeScenarioSearchMatch(scenario: QaSeedScenarioWithSource): QaScenarioSearchMatch {
const config = scenario.execution.config ?? {};
return {
@@ -287,7 +287,7 @@ function resolveMatrixQaStreamingMode(
function isMatrixQaStreamingConfig(
value: MatrixQaConfigOverrides["streaming"],
): value is MatrixQaStreamingConfig {
return Boolean(value && typeof value === "object" && !Array.isArray(value));
return isRecord(value);
}
function resolveMatrixQaStreamingPreviewToolProgress(
@@ -1,7 +1,7 @@
// QA Lab Slack credentials, instrumentation, and channel config.
import type { WebClient } from "@slack/web-api";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
import { uniqueStrings } from "openclaw/plugin-sdk/string-coerce-runtime";
import { asNonArrayRecord, uniqueStrings } from "openclaw/plugin-sdk/string-coerce-runtime";
import {
type SlackQaRuntimeEnv,
type SlackQaConfigOverrides,
@@ -52,9 +52,7 @@ export function parseSlackQaCredentialPayload(payload: unknown): SlackQaRuntimeE
}
export function asPlainRecord(value: unknown): Record<string, unknown> {
return value && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: {};
return asNonArrayRecord(value);
}
type SlackQaPostMessageAttempt = {
@@ -1,4 +1,5 @@
import path from "node:path";
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
import { describe, expect, it, vi } from "vitest";
import { createQaBusState } from "./bus-state.js";
import { runLoadedScenarioFlow } from "./scenario-flow-runner.test-support.js";
@@ -168,8 +169,7 @@ async function runSessionMemoryRankingFlow(params: {
seedQaSessionTranscript: async () => undefined,
forceMemoryIndex,
runAgentPrompt,
normalizeLowercaseStringOrEmpty: (value: unknown) =>
typeof value === "string" ? value.trim().toLowerCase() : "",
normalizeLowercaseStringOrEmpty,
fetchJson,
},
});
@@ -1,4 +1,5 @@
import path from "node:path";
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
import { describe, expect, it } from "vitest";
import { createQaBusState } from "./bus-state.js";
import { runLoadedScenarioFlow } from "./scenario-flow-runner.test-support.js";
@@ -19,8 +20,7 @@ async function runMemoryRecallScenario(recallReply?: string) {
fs: { rm: async () => undefined },
path,
formatMemoryDreamingDay: () => "2026-08-05",
normalizeLowercaseStringOrEmpty: (value: unknown) =>
typeof value === "string" ? value.trim().toLowerCase() : "",
normalizeLowercaseStringOrEmpty,
runAgentPrompt: async (_env: unknown, params: { message: string }) => {
turnCount += 1;
state.addInboundMessage({
@@ -1,3 +1,4 @@
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
import { describe, expect, it, vi } from "vitest";
import { createQaBusState } from "./bus-state.js";
import { runLoadedScenarioFlow } from "./scenario-flow-runner.test-support.js";
@@ -118,8 +119,7 @@ async function runFollowUp(params?: {
runAgentPrompt,
splitModelRef,
normalizeModelRef,
normalizeLowercaseStringOrEmpty: (value: unknown) =>
typeof value === "string" ? value.trim().toLowerCase() : "",
normalizeLowercaseStringOrEmpty,
resolveQaLiveTurnTimeoutMs: (_env: unknown, timeoutMs: number) => timeoutMs,
},
});
@@ -1,3 +1,4 @@
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
import { describe, expect, it, vi } from "vitest";
import { createQaBusState } from "./bus-state.js";
import { hasModelSwitchContinuitySignal } from "./model-switch-eval.js";
@@ -103,8 +104,7 @@ async function runToolContinuity(
},
splitModelRef,
normalizeModelRef,
normalizeLowercaseStringOrEmpty: (value: unknown) =>
typeof value === "string" ? value.trim().toLowerCase() : "",
normalizeLowercaseStringOrEmpty,
resolveQaLiveTurnTimeoutMs: (_env: unknown, timeoutMs: number) => timeoutMs,
hasModelSwitchContinuitySignal,
runAgentPrompt,
@@ -1,4 +1,5 @@
import { join } from "node:path";
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
import { describe, expect, it } from "vitest";
import { createQaBusState } from "./bus-state.js";
import { readQaScenarioById } from "./scenario-catalog.js";
@@ -44,8 +45,7 @@ function createCharacterScenarioApi(
writeFile: async () => undefined,
},
path: { join },
normalizeLowercaseStringOrEmpty: (value: unknown) =>
typeof value === "string" ? value.trim().toLowerCase() : "",
normalizeLowercaseStringOrEmpty,
resolveQaLiveTurnTimeoutMs: () => 10,
waitForOutboundMessage: async (
state: ReturnType<typeof createQaBusState>,
@@ -1,4 +1,6 @@
// Qa Lab tests cover scenario flow runner plugin behavior.
import { coerceErrorMessage } from "openclaw/plugin-sdk/error-runtime";
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
import { describe, expect, it } from "vitest";
import { createQaBusState } from "./bus-state.js";
import { QaSuiteScenarioSkipError } from "./errors.js";
@@ -60,10 +62,8 @@ async function runWebchatTranscriptWait(
}
throw new Error("test condition was not met");
},
normalizeLowercaseStringOrEmpty: (value: unknown) =>
typeof value === "string" ? value.trim().toLowerCase() : "",
formatErrorMessage: (error: unknown) =>
error instanceof Error ? error.message : String(error),
normalizeLowercaseStringOrEmpty,
formatErrorMessage: coerceErrorMessage,
liveTurnTimeoutMs: (_env: unknown, timeoutMs: number) => timeoutMs,
},
});
@@ -284,8 +284,7 @@ function runPlanningEvidenceFixture(
return summary;
},
resolveQaLiveTurnTimeoutMs: (_env: unknown, timeoutMs: number) => timeoutMs,
normalizeLowercaseStringOrEmpty: (value: unknown) =>
typeof value === "string" ? value.trim().toLowerCase() : "",
normalizeLowercaseStringOrEmpty,
runAgentPrompt: async () => ({ started: { runId: "current-run" }, waited: { status: "ok" } }),
},
});
@@ -397,8 +396,7 @@ describe("scenario-flow-runner", () => {
throw new Error("goal artifact has not been written");
},
},
normalizeLowercaseStringOrEmpty: (value: unknown) =>
typeof value === "string" ? value.trim().toLowerCase() : "",
normalizeLowercaseStringOrEmpty,
},
onWaitForOutboundMessage: ({ waitCount, state: currentState }) => {
const currentInbound = currentState
@@ -562,8 +560,7 @@ describe("scenario-flow-runner", () => {
runLoadedScenarioFlow(id, {
state,
api: {
normalizeLowercaseStringOrEmpty: (value: unknown) =>
typeof value === "string" ? value.trim().toLowerCase() : "",
normalizeLowercaseStringOrEmpty,
runAgentPrompt: async () => {
turnCount += 1;
state.addOutboundMessage({
+2 -2
View File
@@ -1,7 +1,7 @@
// Qa Lab plugin module implements suite summary behavior.
import fs from "node:fs/promises";
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
import { asSafeIntegerInRange, isRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
import { QaSuiteArtifactError } from "./errors.js";
import type { QaEvidenceSummaryJson, QaEvidenceTiming } from "./evidence-summary.js";
import type { QaProviderMode } from "./model-selection.js";
@@ -110,7 +110,7 @@ async function readQaSuiteSummaryFile(summaryPath: string): Promise<unknown> {
}
function readNonNegativeCount(value: unknown): number | null {
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : null;
return asSafeIntegerInRange(value, { min: 0 }) ?? null;
}
function assertQaSuiteSummaryHasExecutedScenarios(
+1 -6
View File
@@ -9,6 +9,7 @@ import {
normalizeOpenAICompatibleReasoningReplay,
setQwenChatTemplateThinking,
} from "openclaw/plugin-sdk/provider-stream-shared";
import { asOptionalRecord as asPayloadRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
import {
isQwenTokenPlanDeepSeekV4ModelId,
isQwenTokenPlanGlmModelId,
@@ -26,12 +27,6 @@ type QwenTokenPlanThinkingContract =
| { family: "kimi" }
| { family: "glm"; supportsMax: boolean };
function asPayloadRecord(value: unknown): Record<string, unknown> | undefined {
return value && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: undefined;
}
function resolveQwenThinkingLevel(
thinkingLevel: QwenThinkingLevel,
options: Parameters<StreamFn>[2],
+6 -3
View File
@@ -7,8 +7,11 @@ import {
type ChannelIngressMonitorLifecycle,
} from "openclaw/plugin-sdk/channel-outbound";
import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env";
import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
import { normalizeNullableString as normalizeRawString } from "openclaw/plugin-sdk/string-coerce-runtime";
import {
asPositiveSafeInteger,
isRecord,
normalizeNullableString as normalizeRawString,
} from "openclaw/plugin-sdk/string-coerce-runtime";
import type { SignalSseEvent } from "./client-adapter.js";
import { getOptionalSignalRuntime } from "./runtime.js";
@@ -52,7 +55,7 @@ const SignalIngressPermanentError = createChannelIngressError<
>("SignalIngressPermanentError", { withReason: true });
function normalizeTimestamp(value: unknown): number | null {
return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : null;
return asPositiveSafeInteger(value) ?? null;
}
function parseReceiveEnvelope(event: SignalSseEvent): SignalIngressEnvelope | null {
+5 -4
View File
@@ -12,7 +12,10 @@ import {
type ChannelDmPolicy,
} from "openclaw/plugin-sdk/channel-config-helpers";
import { resolveAccountEntry } from "openclaw/plugin-sdk/routing";
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
import {
asOptionalRecord,
normalizeOptionalString,
} from "openclaw/plugin-sdk/string-coerce-runtime";
import type { SlackAccountSurfaceFields } from "./account-surface-fields.js";
import type { SlackAccountConfig } from "./runtime-api.js";
import { resolveSlackAppToken, resolveSlackBotToken, resolveSlackUserToken } from "./token.js";
@@ -123,9 +126,7 @@ type SlackStreamingConfig = NonNullable<SlackAccountConfig["streaming"]>;
type SlackStreamingConfigValue = SlackStreamingConfig | boolean | string;
function asStreamingConfigObject(value: unknown): SlackStreamingConfig | undefined {
return value && typeof value === "object" && !Array.isArray(value)
? (value as SlackStreamingConfig)
: undefined;
return asOptionalRecord(value) as SlackStreamingConfig | undefined;
}
function asLegacyStreamingScalar(value: unknown): boolean | string | undefined {
@@ -11,6 +11,7 @@ import {
timestampMsToIsoString,
} from "openclaw/plugin-sdk/number-runtime";
import {
asOptionalRecord,
normalizeOptionalString,
normalizeUniqueTrimmedStringList,
} from "openclaw/plugin-sdk/string-coerce-runtime";
@@ -189,13 +190,6 @@ function summarizeRichTextPreview(value: unknown): string | undefined {
return joined.length <= max ? joined : truncateSlackText(joined, max);
}
function readInteractionAction(raw: unknown) {
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
return undefined;
}
return raw as Record<string, unknown>;
}
export function summarizeAction(action: Record<string, unknown>): SlackActionSummary {
const typed = action as {
type?: string;
@@ -431,7 +425,7 @@ function parseSlackBlockAction(params: {
log?: (message: string) => void;
}): ParsedSlackBlockAction | null {
const typedBody = params.body as SlackBlockActionBody;
const typedAction = readInteractionAction(params.action);
const typedAction = asOptionalRecord(params.action);
if (!typedAction) {
params.log?.(
`slack:interaction malformed action payload channel=${typedBody.channel?.id ?? typedBody.container?.channel_id ?? "unknown"} user=${
@@ -871,13 +871,14 @@ vi.mock("openclaw/plugin-sdk/security-runtime", () => ({
resolvePinnedMainDmOwnerFromAllowlist: () => mockedPinnedMainDmOwner,
}));
vi.mock("openclaw/plugin-sdk/string-coerce-runtime", () => {
const isMockRecord = (value: unknown): value is Record<string, unknown> =>
typeof value === "object" && value !== null && !Array.isArray(value);
vi.mock("openclaw/plugin-sdk/string-coerce-runtime", async (importOriginal) => {
const { asOptionalRecord, isRecord } =
await importOriginal<typeof import("openclaw/plugin-sdk/string-coerce-runtime")>();
const normalizeMockLowercaseString = (value?: string) => value?.toLowerCase();
const readMockOptionalString = (value?: string) => value;
return {
isRecord: isMockRecord,
asOptionalRecord,
isRecord,
normalizeOptionalLowercaseString: normalizeMockLowercaseString,
normalizeOptionalString: readMockOptionalString,
};
+2 -1
View File
@@ -2,6 +2,7 @@
import { EventEmitter } from "node:events";
import type { ClientRequest, IncomingMessage, RequestOptions } from "node:http";
import { PassThrough } from "node:stream";
import { coerceErrorMessage } from "openclaw/plugin-sdk/error-runtime";
import { describe, it, expect, vi, beforeAll, beforeEach, afterEach } from "vitest";
const ssrfMocks = {
@@ -26,7 +27,7 @@ vi.mock("node:http", async () => {
});
vi.mock("openclaw/plugin-sdk/ssrf-runtime", () => ({
formatErrorMessage: (err: unknown) => (err instanceof Error ? err.message : String(err)),
formatErrorMessage: coerceErrorMessage,
resolvePinnedHostnameWithPolicy: ssrfMocks.resolvePinnedHostnameWithPolicy,
}));
+2 -2
View File
@@ -1,5 +1,6 @@
import { MeetingPlatformAdapter } from "openclaw/plugin-sdk/meeting-runtime";
import { normalizeAgentId } from "openclaw/plugin-sdk/routing";
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
import { Type } from "typebox";
import { teamsMeetingsConfig } from "./src/config.js";
import { TeamsMeetingsInvalidRequestError, teamsMeetingsInvalidRequest } from "./src/errors.js";
@@ -26,8 +27,7 @@ export default MeetingPlatformAdapter.createPluginShellEntry({
message: Type.Optional(Type.String({ description: "Instructions to speak" })),
}),
resolveGatewayTimeoutMs: teamsMeetingsConfig.resolveGatewayOperationTimeoutMs,
normalizeRequesterSessionKey: (value) =>
typeof value === "string" && value.trim() ? value.trim() : undefined,
normalizeRequesterSessionKey: normalizeOptionalString,
normalizeToolAgentId: (agentId) => (agentId ? normalizeAgentId(agentId) : undefined),
resolveToolRuntime: async (api, agentId) => {
const trustedRouting = Boolean(agentId && agentId !== "main");
+4 -9
View File
@@ -11,19 +11,14 @@ vi.mock("openclaw/plugin-sdk/text-utility-runtime", () => ({
fetchWithTimeout: fetchWithTimeoutMock,
}));
vi.mock("openclaw/plugin-sdk/string-coerce-runtime", () => {
vi.mock("openclaw/plugin-sdk/string-coerce-runtime", async (importOriginal) => {
const { normalizeOptionalString } =
await importOriginal<typeof import("openclaw/plugin-sdk/string-coerce-runtime")>();
const isMockRecord = (value: unknown): value is Record<string, unknown> =>
typeof value === "object" && value !== null;
const normalizeMockOptionalString = (value: unknown) => {
if (typeof value !== "string") {
return undefined;
}
const trimmed = value.trim();
return trimmed ? trimmed : undefined;
};
return {
isRecord: isMockRecord,
normalizeOptionalString: normalizeMockOptionalString,
normalizeOptionalString,
};
});
@@ -12,6 +12,7 @@ import {
} from "openclaw/plugin-sdk/conversation-runtime";
import { isApprovalNotFoundError } from "openclaw/plugin-sdk/error-runtime";
import { logVerbose, sleepWithAbort } from "openclaw/plugin-sdk/runtime-env";
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
import type { TelegramApprovalCallback } from "./approval-callback-data.js";
import {
buildTelegramCanonicalApprovalTerminalText,
@@ -420,11 +421,7 @@ const updateMultiSelectKeyboard = (
);
const resolvePluginCallbackSubmitText = (submitText: unknown): string | undefined => {
if (typeof submitText !== "string") {
return undefined;
}
const trimmed = submitText.trim();
return trimmed ? trimmed : undefined;
return normalizeOptionalString(submitText);
};
const isReplySessionInitConflictError = (err: unknown): boolean =>
@@ -10,6 +10,7 @@ import {
resolveAmbientTranscriptWatermarkKey,
type SessionEntry,
} from "openclaw/plugin-sdk/session-store-runtime";
import { asFiniteNumber } from "openclaw/plugin-sdk/string-coerce-runtime";
import { stripInlineDirectiveTagsForDelivery } from "openclaw/plugin-sdk/text-chunking";
import { resolveDefaultModelForAgent } from "./bot-handlers.agent.runtime.js";
import type { RegisterTelegramHandlerParams } from "./bot-handlers.types.js";
@@ -98,7 +99,7 @@ export type ResolvePromptContextAmbientWatermarkParams = {
};
export const normalizePromptContextMinTimestampMs = (timestampMs?: number) =>
typeof timestampMs === "number" && Number.isFinite(timestampMs) ? timestampMs : undefined;
asFiniteNumber(timestampMs);
export function promptContextBoundaryOptions(
timestampMs?: number,
@@ -1,5 +1,6 @@
// Telegram tests cover bot.create telegram bot.media group skip warning plugin behavior.
import { setTimeout as delay } from "node:timers/promises";
import { coerceErrorMessage } from "openclaw/plugin-sdk/error-runtime";
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { telegramBotInfoForTest } from "./bot.create-telegram-bot.test-support.js";
@@ -21,7 +22,7 @@ vi.mock("./bot/delivery.resolve-media.runtime.js", async () => {
);
return {
readRemoteMediaBuffer: (...args: unknown[]) => readRemoteMediaBuffer(...args),
formatErrorMessage: (err: unknown) => (err instanceof Error ? err.message : String(err)),
formatErrorMessage: coerceErrorMessage,
logVerbose: () => {},
MediaFetchError: actual.MediaFetchError,
resolveTelegramApiBase: (apiRoot?: string) =>
@@ -1,5 +1,6 @@
import { GrammyError } from "grammy";
import type { Message } from "grammy/types";
import { coerceErrorMessage } from "openclaw/plugin-sdk/error-runtime";
import { sleepWithAbort } from "openclaw/plugin-sdk/runtime-env";
// Telegram tests cover delivery.resolve media retry plugin behavior.
import { createRequireRecord } from "openclaw/plugin-sdk/test-fixtures";
@@ -56,7 +57,7 @@ vi.mock("./delivery.resolve-media.runtime.js", () => {
}
return {
readRemoteMediaBuffer: (...args: unknown[]) => readRemoteMediaBuffer(...args),
formatErrorMessage: (err: unknown) => (err instanceof Error ? err.message : String(err)),
formatErrorMessage: coerceErrorMessage,
logVerbose: () => {},
MediaFetchError,
resolveTelegramApiBase: (apiRoot?: string) =>
@@ -1,4 +1,5 @@
import type { Message } from "grammy/types";
import { coerceErrorMessage } from "openclaw/plugin-sdk/error-runtime";
import { sleepWithAbort } from "openclaw/plugin-sdk/runtime-env";
// Telegram tests cover delivery.resolve media retry plugin behavior.
import { createRequireRecord } from "openclaw/plugin-sdk/test-fixtures";
@@ -55,7 +56,7 @@ vi.mock("./delivery.resolve-media.runtime.js", () => {
}
return {
readRemoteMediaBuffer: (...args: unknown[]) => readRemoteMediaBuffer(...args),
formatErrorMessage: (err: unknown) => (err instanceof Error ? err.message : String(err)),
formatErrorMessage: coerceErrorMessage,
logVerbose: () => {},
MediaFetchError,
resolveTelegramApiBase: (apiRoot?: string) =>
@@ -2,9 +2,8 @@
import type { TelegramNetworkConfig } from "openclaw/plugin-sdk/config-contracts";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
vi.mock("openclaw/plugin-sdk/runtime-env", () => ({
isTruthyEnvValue: (value: string | undefined) =>
typeof value === "string" && /^(1|true|yes|on)$/i.test(value.trim()),
vi.mock("openclaw/plugin-sdk/runtime-env", async (importOriginal) => ({
...(await importOriginal<typeof import("openclaw/plugin-sdk/runtime-env")>()),
isWSL2Sync: vi.fn(() => false),
}));
+2 -1
View File
@@ -2,6 +2,7 @@
import { GrammyError } from "grammy";
import type { MessageEntity } from "grammy/types";
import { formatErrorMessage } from "openclaw/plugin-sdk/ssrf-runtime";
import { asFiniteNumber } from "openclaw/plugin-sdk/string-coerce-runtime";
import { buildTelegramThreadParams, type TelegramThreadSpec } from "./bot/helpers.js";
import { normalizeTelegramReplyToMessageId } from "./outbound-params.js";
@@ -118,7 +119,7 @@ export function getTelegramNativeQuoteReplyMessageId(
return undefined;
}
const messageId = (replyParameters as { message_id?: unknown }).message_id;
return typeof messageId === "number" && Number.isFinite(messageId) ? messageId : undefined;
return asFiniteNumber(messageId);
}
export function isTelegramQuoteParamError(err: unknown): boolean {
+4 -8
View File
@@ -11,7 +11,7 @@ import {
resolveEnabledConfiguredAccountId,
type AccountStatusSnapshot,
} from "openclaw/plugin-sdk/status-helpers";
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
import { asFiniteNumber, normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
const TELEGRAM_POLLING_CONNECT_GRACE_MS = 120_000;
const TELEGRAM_POLLING_STALE_TRANSPORT_MS = 30 * 60_000;
@@ -41,10 +41,6 @@ type TelegramGroupMembershipAuditSummary = {
}>;
};
function asFiniteNumberOrNull(value: unknown): number | null {
return typeof value === "number" && Number.isFinite(value) ? value : null;
}
function appendTelegramRuntimeError(message: string, lastError: unknown): string {
const error = normalizeOptionalString(lastError);
return error ? `${message}: ${error}` : message;
@@ -69,8 +65,8 @@ function collectTelegramPollingRuntimeIssues(params: {
return;
}
const lastStartAt = asFiniteNumberOrNull(account.lastStartAt);
const lastTransportActivityAt = asFiniteNumberOrNull(account.lastTransportActivityAt);
const lastStartAt = asFiniteNumber(account.lastStartAt) ?? null;
const lastTransportActivityAt = asFiniteNumber(account.lastTransportActivityAt) ?? null;
const fix = `Run: ${formatCliCommand("openclaw channels status --probe")} (or restart the gateway). Check the bot token, proxy/network settings, and logs if it persists.`;
if (account.connected === false) {
@@ -129,7 +125,7 @@ function collectTelegramWebhookRuntimeIssues(params: {
return;
}
const lastStartAt = asFiniteNumberOrNull(account.lastStartAt);
const lastStartAt = asFiniteNumber(account.lastStartAt) ?? null;
const withinStartupGrace =
lastStartAt != null && now - lastStartAt < TELEGRAM_WEBHOOK_CONNECT_GRACE_MS;
if (withinStartupGrace) {
@@ -4,6 +4,7 @@ import {
sleepWithAbort,
type BackoffPolicy,
} from "openclaw/plugin-sdk/runtime-env";
import { asSafeIntegerInRange } from "openclaw/plugin-sdk/string-coerce-runtime";
const OFFSET_PERSIST_RETRY_POLICY: BackoffPolicy = {
initialMs: 250,
@@ -21,10 +22,7 @@ type TelegramUpdateOffsetPersistenceOptions = {
};
export function normalizeTelegramUpdateId(value: number | null): number | null {
if (value === null || !Number.isSafeInteger(value) || value < 0) {
return null;
}
return value;
return asSafeIntegerInRange(value, { min: 0 }) ?? null;
}
export function createTelegramUpdateOffsetPersistence(
+1 -1
View File
@@ -1,8 +1,8 @@
// Tlon plugin module implements discovery behavior.
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime";
import { asNullableRecord as asRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
import type { Foreigns } from "../urbit/foreigns.js";
import { formatErrorMessage } from "./utils.js";
interface InitData {
channels: string[];
+2 -1
View File
@@ -1,7 +1,8 @@
// Tlon plugin module implements history behavior.
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime";
import { asNullableRecord as asRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
import { extractMessageText, formatErrorMessage } from "./utils.js";
import { extractMessageText } from "./utils.js";
/**
* Format a number as @ud (with dots every 3 digits from the right)
+1 -1
View File
@@ -4,6 +4,7 @@ import {
bindIngressLifecycleToReplyOptions,
waitUntilAbort,
} from "openclaw/plugin-sdk/channel-outbound";
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
import type { GetReplyOptions, ReplyPayload } from "openclaw/plugin-sdk/reply-runtime";
import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime";
import { sleepWithAbort } from "openclaw/plugin-sdk/runtime-env";
@@ -44,7 +45,6 @@ import {
shouldMigrateTlonSetting,
} from "./settings-helpers.js";
import { createActiveSnapshotTracker, createParticipatedThreadTracker } from "./tracking.js";
import { formatErrorMessage } from "./utils.js";
import {
extractMessageText,
formatModelName,
-3
View File
@@ -11,7 +11,6 @@ import {
type StableChannelIngressIdentityParams,
} from "openclaw/plugin-sdk/channel-ingress-runtime";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
import { formatErrorMessage as sharedFormatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
// Tlon helper module supports utils behavior.
import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
import { asNullableRecord, readStringField } from "openclaw/plugin-sdk/string-coerce-runtime";
@@ -243,8 +242,6 @@ export async function resolveAuthorizedMessageText(params: {
return citedContent + rawText;
}
export const formatErrorMessage = sharedFormatErrorMessage;
// Helper to recursively extract text from inline content
function renderInlineItem(
item: unknown,
+4 -4
View File
@@ -1,13 +1,13 @@
// Together tests cover together plugin behavior.
import { completeSimple, type Model } from "openclaw/plugin-sdk/llm";
import { isTruthyEnvValue } from "openclaw/plugin-sdk/runtime-env";
import { describe, expect, it } from "vitest";
import { TOGETHER_BASE_URL, TOGETHER_MODEL_CATALOG } from "./models.js";
const TOGETHER_KEY = process.env.TOGETHER_API_KEY ?? "";
const LIVE = ["LIVE", "OPENCLAW_LIVE_TEST", "TOGETHER_LIVE_TEST"].some((name) => {
const value = process.env[name]?.trim().toLowerCase();
return value === "1" || value === "true" || value === "yes" || value === "on";
});
const LIVE = ["LIVE", "OPENCLAW_LIVE_TEST", "TOGETHER_LIVE_TEST"].some((name) =>
isTruthyEnvValue(process.env[name]),
);
const TOGETHER_LIVE_TIMEOUT_MS = 45_000;
const describeLive = LIVE && TOGETHER_KEY ? describe : describe.skip;
@@ -3,6 +3,7 @@ import { EventEmitter } from "node:events";
import fsSync from "node:fs";
import os from "node:os";
import path from "node:path";
import { coerceErrorMessage } from "openclaw/plugin-sdk/error-runtime";
import { createChannelIngressQueueForTests } from "openclaw/plugin-sdk/plugin-state-test-runtime";
import { resetLogger, setLoggerOverride } from "openclaw/plugin-sdk/runtime-env";
import { afterEach, beforeEach, expect, vi } from "vitest";
@@ -238,7 +239,7 @@ vi.mock("./session.js", async () => {
}),
waitForWaConnection: vi.fn().mockResolvedValue(undefined),
getStatusCode: vi.fn(() => 500),
formatError: (err: unknown) => (err instanceof Error ? err.message : String(err)),
formatError: coerceErrorMessage,
};
});
@@ -1,6 +1,7 @@
// Whatsapp tests cover qa driver plugin behavior.
import { EventEmitter } from "node:events";
import type { proto, WAMessage } from "baileys";
import { coerceErrorMessage } from "openclaw/plugin-sdk/error-runtime";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { startWhatsAppQaDriverSession, type WhatsAppQaDriverSession } from "./qa-driver.runtime.js";
import { DEFAULT_WHATSAPP_SOCKET_TIMING } from "./socket-timing.js";
@@ -22,7 +23,7 @@ const mocks = vi.hoisted(() => ({
vi.mock("./session.js", () => ({
createWaSocket: mocks.createWaSocket,
formatError: (error: unknown) => (error instanceof Error ? error.message : String(error)),
formatError: coerceErrorMessage,
getStatusCode: (error: unknown) =>
(error as { output?: { statusCode?: number } } | undefined)?.output?.statusCode,
waitForWaConnection: mocks.waitForWaConnection,
+3 -6
View File
@@ -1,4 +1,5 @@
// ACP Core module implements meta behavior.
import { asFiniteNumber, asSafeIntegerInRange } from "@openclaw/normalization-core/number-coercion";
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
function readMetaValue<T>(
@@ -39,9 +40,7 @@ export function readMetadataNumber(
meta: Record<string, unknown> | null | undefined,
keys: string[],
): number | undefined {
return readMetaValue(meta, keys, (value) =>
typeof value === "number" && Number.isFinite(value) ? value : undefined,
);
return readMetaValue(meta, keys, asFiniteNumber);
}
/** Reads the first safe non-negative integer metadata value, preserving zero. */
@@ -49,7 +48,5 @@ export function readNonNegativeInteger(
meta: Record<string, unknown> | null | undefined,
keys: string[],
): number | undefined {
return readMetaValue(meta, keys, (value) =>
typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : undefined,
);
return readMetaValue(meta, keys, (value) => asSafeIntegerInRange(value, { min: 0 }));
}
+10 -19
View File
@@ -9,6 +9,7 @@ import type {
ToolResultMessage,
} from "@openclaw/llm-core";
import type { EventStream as SourceEventStream } from "@openclaw/llm-core";
import { coerceErrorMessage } from "@openclaw/normalization-core/error-coercion";
import { asOptionalRecord } from "@openclaw/normalization-core/record-coerce";
import { TranscriptNotContinuableError } from "./errors.js";
import { uuidv7 } from "./harness/session/uuid.js";
@@ -1332,7 +1333,7 @@ async function prepareToolCall(
} catch (error) {
return {
kind: "immediate",
result: createErrorToolResult(error instanceof Error ? error.message : String(error)),
result: createErrorToolResult(coerceErrorMessage(error)),
isError: true,
};
}
@@ -1360,11 +1361,7 @@ async function validateToolCallForBatchAdmission(
outcome: {
kind: "immediate",
result: createErrorToolResult(
signal?.aborted
? "Operation aborted"
: resolution.error instanceof Error
? resolution.error.message
: String(resolution.error),
signal?.aborted ? "Operation aborted" : coerceErrorMessage(resolution.error),
),
isError: true,
},
@@ -1390,7 +1387,7 @@ async function validateToolCallForBatchAdmission(
kind: "immediate",
outcome: {
kind: "immediate",
result: createErrorToolResult(error instanceof Error ? error.message : String(error)),
result: createErrorToolResult(coerceErrorMessage(error)),
isError: true,
},
};
@@ -1404,7 +1401,7 @@ async function validateToolCallForBatchAdmission(
kind: "immediate",
outcome: {
kind: "immediate",
result: createErrorToolResult(error instanceof Error ? error.message : String(error)),
result: createErrorToolResult(coerceErrorMessage(error)),
isError: true,
errorKind: "argument-validation",
},
@@ -1452,7 +1449,7 @@ async function prepareToolCallExecution(
return {
kind: "immediate",
outcome: {
result: createErrorToolResult(error instanceof Error ? error.message : String(error)),
result: createErrorToolResult(coerceErrorMessage(error)),
isError: true,
executionStarted: false,
},
@@ -1508,7 +1505,7 @@ async function prepareToolCallExecution(
throw implementationStartError.error;
}
return {
result: createErrorToolResult(error instanceof Error ? error.message : String(error)),
result: createErrorToolResult(coerceErrorMessage(error)),
isError: true,
executionStarted,
...(executionStarted && signal?.aborted && error === signal.reason
@@ -1570,11 +1567,7 @@ async function prepareToolCallExecution(
return {
kind: "immediate",
outcome: {
result: createErrorToolResult(
internalPreparation.outcome.error instanceof Error
? internalPreparation.outcome.error.message
: String(internalPreparation.outcome.error),
),
result: createErrorToolResult(coerceErrorMessage(internalPreparation.outcome.error)),
isError: true,
executionStarted: false,
},
@@ -1627,7 +1620,7 @@ async function finalizeExecutedToolCall(
isError = afterResult.isError ?? isError;
}
} catch (error) {
result = createErrorToolResult(error instanceof Error ? error.message : String(error));
result = createErrorToolResult(coerceErrorMessage(error));
isError = true;
}
}
@@ -1692,9 +1685,7 @@ async function finalizeToolCallOutcome(
isError: afterResult.isError ?? finalized.isError,
};
} catch (error) {
const errorResult = createErrorToolResult(
error instanceof Error ? error.message : String(error),
);
const errorResult = createErrorToolResult(coerceErrorMessage(error));
return {
...finalized,
result: {
+2 -1
View File
@@ -1,3 +1,4 @@
import { asNonNegativeFiniteNumber } from "@openclaw/normalization-core/number-coercion";
import type { Usage } from "../types.js";
type AnthropicUsagePayload = {
@@ -31,7 +32,7 @@ export type AnthropicIterationUsageResult =
| { state: "valid"; usage: AnthropicIterationUsageSnapshot };
export function readAnthropicUsageTokenCount(value: unknown): number | undefined {
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
return asNonNegativeFiniteNumber(value);
}
export function readAnthropicCacheWriteUsage(
@@ -6,6 +6,7 @@
* package and managed transports from drifting on token buckets, service-tier pricing, or future
* terminal-event semantics.
*/
import { asFiniteNumber } from "@openclaw/normalization-core/number-coercion";
import type OpenAI from "openai";
import type { StopReason, Usage } from "../types.js";
@@ -53,10 +54,7 @@ export function mapResponsesTerminalUsage(
export function readResponsesReasoningTokens(
usage: ResponsesTerminalUsagePayload | undefined | null,
): number | undefined {
const reasoningTokens = usage?.output_tokens_details?.reasoning_tokens;
return typeof reasoningTokens === "number" && Number.isFinite(reasoningTokens)
? reasoningTokens
: undefined;
return asFiniteNumber(usage?.output_tokens_details?.reasoning_tokens);
}
function mapResponsesTerminalStopReason(

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