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

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