refactor: consolidate remaining coercion helpers (#122020)

This commit is contained in:
Peter Steinberger
2026-08-11 10:22:01 -07:00
committed by GitHub
parent 24bbb416b5
commit cad77fb39c
234 changed files with 1210 additions and 1572 deletions
+8
View File
@@ -619,6 +619,11 @@ jobs:
runtime: "node",
task: "max-lines-ratchet",
},
{
check_name: "checks-fast-coercion-helpers",
runtime: "node",
task: "coercion-helpers",
},
]
: [];
if (runNodeFull) {
@@ -1759,6 +1764,9 @@ jobs:
ci-routing)
pnpm test src/commands/status.scan-result.test.ts src/scripts/ci-changed-scope*.test.ts test/scripts/changed-lanes.test.ts test/scripts/ci-changed-node-test-plan.test.ts test/scripts/ci-run-node-test-shard.test.ts test/scripts/ci-workflow-guards.test.ts test/scripts/run-vitest.test.ts test/scripts/test-projects.test.ts
;;
coercion-helpers)
pnpm check:coercion-helpers
;;
max-lines-ratchet)
if ! has_package_script "check:max-lines-ratchet"; then
echo "Current CI targets must provide check:max-lines-ratchet." >&2
@@ -1 +1 @@
{"contentHash":"6b0a039cf6246df1ec52695441900203343ddc3862ef0c9c100f08cab004bfe7","entrypoint":"agent-harness-runtime","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime"}
{"contentHash":"f0501615d366cab1ceffabfcbb580b9a9f1008bbd4e24a46e1fc10d73e9cd2a3","entrypoint":"agent-harness-runtime","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime"}
+1 -1
View File
@@ -1 +1 @@
{"contentHash":"817cf688ac6ab0bcde36b59ba9d20e6511fdc82f58f727557cd33097c49b1940","entrypoint":"agent-harness","importSpecifier":"openclaw/plugin-sdk/agent-harness"}
{"contentHash":"ae5e7178a886a346e7a7d88e80344b84c84679e156d08eb863c8a5e2bbffe03c","entrypoint":"agent-harness","importSpecifier":"openclaw/plugin-sdk/agent-harness"}
+1 -1
View File
@@ -1 +1 @@
{"contentHash":"f38505150fe39946aa11b2963190d3c4838783086be6c76bbca9194447afaa63","entrypoint":"agent-runtime","importSpecifier":"openclaw/plugin-sdk/agent-runtime"}
{"contentHash":"2f175fa56f7836b6dc666ef556b964ecfa83f6f81f683340907c7456c8f301ea","entrypoint":"agent-runtime","importSpecifier":"openclaw/plugin-sdk/agent-runtime"}
+1 -1
View File
@@ -1 +1 @@
{"contentHash":"3d6ce8758b33237aad8417ec2731c2677cabc092f3b71671b4df28663f21c2f7","entrypoint":"channel-core","importSpecifier":"openclaw/plugin-sdk/channel-core"}
{"contentHash":"9360377bb569d5f8f53151152e4b1e27e860ebc38d176ee3d71cbf3461c79ca8","entrypoint":"channel-core","importSpecifier":"openclaw/plugin-sdk/channel-core"}
@@ -1 +1 @@
{"contentHash":"c7f13d09cb45ae35ce5855ed7576905ca2ab4cca11b589647e8bbf84ce8d8ded","entrypoint":"channel-entry-contract","importSpecifier":"openclaw/plugin-sdk/channel-entry-contract"}
{"contentHash":"5e756ed6e46b362058d9ca438cf858c463bb01fd55269b0b019341fe0075bcda","entrypoint":"channel-entry-contract","importSpecifier":"openclaw/plugin-sdk/channel-entry-contract"}
@@ -1 +1 @@
{"contentHash":"d4b4c9c5f145aa60c35f928f090f74959c6c3f1745fe164bb8146b6d341e8d88","entrypoint":"channel-inbound","importSpecifier":"openclaw/plugin-sdk/channel-inbound"}
{"contentHash":"3523cddf2dad5146687de9564879903e20224b140f4a9756ba57e76034c6490a","entrypoint":"channel-inbound","importSpecifier":"openclaw/plugin-sdk/channel-inbound"}
@@ -1 +1 @@
{"contentHash":"d2e3782705a71a08616f0c59ac1be8d8e03b1ec004c604bcc80a17567613dd71","entrypoint":"channel-message","importSpecifier":"openclaw/plugin-sdk/channel-message"}
{"contentHash":"5091fb6b03b3e1b3cb6b51323cb7bd991f8022d1aac479b4a67982993dec79cf","entrypoint":"channel-message","importSpecifier":"openclaw/plugin-sdk/channel-message"}
@@ -1 +1 @@
{"contentHash":"7a6122d34d21e433c7a1a0996879c0964b3a22f508a793a53ffe6404ddf4b5ef","entrypoint":"channel-outbound","importSpecifier":"openclaw/plugin-sdk/channel-outbound"}
{"contentHash":"33c41deadb29159ecaf9e40c0053d43a81531ca48145f11217f811e4232c5cf2","entrypoint":"channel-outbound","importSpecifier":"openclaw/plugin-sdk/channel-outbound"}
@@ -1 +1 @@
{"contentHash":"f4a7174aefe70f8bf04bfb293ca92ab9f0f99b6073a8149eee45ce902b99b5ee","entrypoint":"channel-plugin-common","importSpecifier":"openclaw/plugin-sdk/channel-plugin-common"}
{"contentHash":"c6e8524442d54875e1ce5ec211394472c2e2734fbfbebe320b069346374b922d","entrypoint":"channel-plugin-common","importSpecifier":"openclaw/plugin-sdk/channel-plugin-common"}
+1 -1
View File
@@ -1 +1 @@
{"contentHash":"c5756c96150425246ed16313fdfc11e6aa780634018a57e920fa5d7d43fcd469","entrypoint":"core","importSpecifier":"openclaw/plugin-sdk/core"}
{"contentHash":"1edb0e67f51c958fff1d3ace6e47debb831555720208ebfb732cf65de2bb333a","entrypoint":"core","importSpecifier":"openclaw/plugin-sdk/core"}
+1 -1
View File
@@ -1 +1 @@
{"contentHash":"d01d8e0dd8da33679bf50faa64244e5c2c95f2c133ffc73f587e0b43176c43dd","entrypoint":"discord","importSpecifier":"openclaw/plugin-sdk/discord"}
{"contentHash":"243be23ce58934cc502148cde9b747c4e8d26d176526e99dda383f16f20fc124","entrypoint":"discord","importSpecifier":"openclaw/plugin-sdk/discord"}
@@ -1 +1 @@
{"contentHash":"be64b90e0781977ab81a78be2238569ea7d51555b8784a2be600cac60f8422e0","entrypoint":"inbound-reply-dispatch","importSpecifier":"openclaw/plugin-sdk/inbound-reply-dispatch"}
{"contentHash":"a27b168ddc12affa72e1d646d36ea8ef65f18f2de7a7397f0f9e816659e54c0e","entrypoint":"inbound-reply-dispatch","importSpecifier":"openclaw/plugin-sdk/inbound-reply-dispatch"}
@@ -1 +1 @@
{"contentHash":"d2fd18514981c62a3f5e2937b8fc42f9679d7cec9c133198754dfacf54bdfd33","entrypoint":"meeting-runtime","importSpecifier":"openclaw/plugin-sdk/meeting-runtime"}
{"contentHash":"336c432da880945a8dccc3f6e0c96cc062b4afdd96b73b35382ffedc537762be","entrypoint":"meeting-runtime","importSpecifier":"openclaw/plugin-sdk/meeting-runtime"}
+1 -1
View File
@@ -1 +1 @@
{"contentHash":"4474179d9e5549947f7ef57a950be9ad426193c8ce9f23187d5d7c535f3d4af5","entrypoint":"plugin-entry","importSpecifier":"openclaw/plugin-sdk/plugin-entry"}
{"contentHash":"66b344dc4d56bdb7d3c1ddccb64899a013f98e1dda7f734e6b75de49c41ba0fc","entrypoint":"plugin-entry","importSpecifier":"openclaw/plugin-sdk/plugin-entry"}
+1 -1
View File
@@ -1 +1 @@
{"contentHash":"91fce99cea0d4ed66d7971f246ab66e0ec2c139caeb78c89b21a434908e2ca9f","entrypoint":"plugin-runtime","importSpecifier":"openclaw/plugin-sdk/plugin-runtime"}
{"contentHash":"e7788890d54d5784f4bce552c7143a0083cd1cdd46f126b98d66ad8357327e51","entrypoint":"plugin-runtime","importSpecifier":"openclaw/plugin-sdk/plugin-runtime"}
@@ -1 +1 @@
{"contentHash":"927ae41bc0eeb5a07685238f4732c0caa639e28e10358555eba1096d22121172","entrypoint":"provider-catalog-runtime","importSpecifier":"openclaw/plugin-sdk/provider-catalog-runtime"}
{"contentHash":"4b0295b65533f821a02e3399298424b35c2c75082e4d53d57ea88ff2f84d74ad","entrypoint":"provider-catalog-runtime","importSpecifier":"openclaw/plugin-sdk/provider-catalog-runtime"}
@@ -1 +1 @@
{"contentHash":"e7d1347fa4d9bbc2715c95473eada52b6637ff538946e52f3cfd8ef2bf4cdc7c","entrypoint":"string-coerce-runtime","importSpecifier":"openclaw/plugin-sdk/string-coerce-runtime"}
{"contentHash":"14d56ffc88afba3e8c83ace0894f0187860a45b739d562b96e290d2235c45294","entrypoint":"string-coerce-runtime","importSpecifier":"openclaw/plugin-sdk/string-coerce-runtime"}
+1 -1
View File
@@ -1 +1 @@
{"contentHash":"7b9e970e85cb5ac7d407bd5bf70e85b38407ba9d26b66a5bb8d830623cf806d7","entrypoint":"tool-plugin","importSpecifier":"openclaw/plugin-sdk/tool-plugin"}
{"contentHash":"c489a3d36e88b55b29c9019df733662acb67ec95e2fd0a99d2b208ac123801f5","entrypoint":"tool-plugin","importSpecifier":"openclaw/plugin-sdk/tool-plugin"}
@@ -1 +1 @@
{"contentHash":"5cfa7051b82c9d49810529f1522f584f28bc45b184416b8a898017423ebba415","entrypoint":"webhook-ingress","importSpecifier":"openclaw/plugin-sdk/webhook-ingress"}
{"contentHash":"73b1700f912dfb34554f7a9e72bca0ff9e1ea08360f32c981519488a25023f2c","entrypoint":"webhook-ingress","importSpecifier":"openclaw/plugin-sdk/webhook-ingress"}
+4 -9
View File
@@ -4,7 +4,10 @@ import type {
SessionCatalogTranscriptItem,
SessionsCatalogReadResult,
} from "openclaw/plugin-sdk/session-catalog";
import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
import {
isRecord,
normalizeBoundedOptionalString as optionalPiString,
} from "openclaw/plugin-sdk/string-coerce-runtime";
import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
import { listPiSummaryPage, readPiSessionById } from "./pi-session-store.js";
import { parsePiSessionTimestampMs } from "./pi-session-timestamp.js";
@@ -20,14 +23,6 @@ const SESSION_ID_PATTERN = /^(?!-)[A-Za-z0-9._:-]{1,256}$/u;
export type PiSessionPage = { sessions: SessionCatalogSession[]; nextCursor?: string };
function optionalPiString(value: unknown, maxLength: number): string | undefined {
if (typeof value !== "string") {
return undefined;
}
const trimmed = value.trim();
return trimmed && trimmed.length <= maxLength ? trimmed : undefined;
}
function boundedLimit(value: unknown, fallback = DEFAULT_PAGE_LIMIT): number {
if (value === undefined) {
return fallback;
+4 -9
View File
@@ -1,15 +1,10 @@
import { readFileSync, statSync } from "node:fs";
import os from "node:os";
import path from "node:path";
import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
function readBoundedString(value: unknown, maxLength: number): string | undefined {
if (typeof value !== "string") {
return undefined;
}
const trimmed = value.trim();
return trimmed && trimmed.length <= maxLength ? trimmed : undefined;
}
import {
isRecord,
normalizeBoundedOptionalString as readBoundedString,
} from "openclaw/plugin-sdk/string-coerce-runtime";
function piHome(env: NodeJS.ProcessEnv): string {
const configured = process.platform === "win32" ? env.USERPROFILE?.trim() : env.HOME?.trim();
+4 -9
View File
@@ -2,7 +2,10 @@ import { createReadStream } from "node:fs";
import fs from "node:fs/promises";
import path from "node:path";
import type { SessionCatalogSession } from "openclaw/plugin-sdk/session-catalog";
import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
import {
isRecord,
normalizeBoundedOptionalString as readBoundedString,
} from "openclaw/plugin-sdk/string-coerce-runtime";
import { piAcpSessionStoreRoot, piSessionStore } from "./pi-session-paths.js";
import { parsePiSessionTimestampMs } from "./pi-session-timestamp.js";
@@ -85,14 +88,6 @@ function cacheSummary(file: string, value: CachedSummary): void {
}
}
function readBoundedString(value: unknown, maxLength: number): string | undefined {
if (typeof value !== "string") {
return undefined;
}
const trimmed = value.trim();
return trimmed && trimmed.length <= maxLength ? trimmed : undefined;
}
async function discoverPiSessionFiles(
env: NodeJS.ProcessEnv,
): Promise<{ root: string; files: string[] }> {
+2 -6
View File
@@ -1,10 +1,6 @@
import { asFiniteNumber } from "openclaw/plugin-sdk/string-coerce-runtime";
import { parseDateFirstTimestampMs } from "openclaw/plugin-sdk/number-runtime";
/** Preserve Pi JSONL's date-first string contract while accepting numeric millisecond values. */
export function parsePiSessionTimestampMs(value: unknown): number | undefined {
if (typeof value === "string") {
const parsed = Date.parse(value);
return Number.isFinite(parsed) ? parsed : undefined;
}
return asFiniteNumber(value);
return parseDateFirstTimestampMs(value);
}
+1 -13
View File
@@ -3,6 +3,7 @@ import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { expectDefined } from "@openclaw/normalization-core";
import { toErrorObject as toLintErrorObject } from "openclaw/plugin-sdk/error-runtime";
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry";
import type { OpenKeyedStoreOptions } from "openclaw/plugin-sdk/plugin-state-runtime";
import {
@@ -5882,17 +5883,4 @@ describe("active-memory plugin", () => {
});
});
function toLintErrorObject(value: unknown, fallbackMessage: string): Error {
if (value instanceof Error) {
return value;
}
if (typeof value === "string") {
return new Error(value);
}
const error = new Error(fallbackMessage, { cause: value });
if ((typeof value === "object" && value !== null) || typeof value === "function") {
Object.assign(error, value);
}
return error;
}
/* oxlint-disable max-lines -- TODO: split this grandfathered oversized file. */
@@ -58,7 +58,7 @@ export function collectTranscriptText(value: unknown, fragments: string[]): void
export function parseTranscriptLine(
line: Buffer,
optionalString: (value: unknown, maxLength?: number) => string | undefined,
optionalString: (value: unknown, maxLength: number) => string | undefined,
): ClaudeTranscriptItem | undefined {
let raw: unknown;
try {
+11 -13
View File
@@ -3,6 +3,7 @@ import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { resolveDefaultAgentId } from "openclaw/plugin-sdk/agent-runtime";
import { parseDateFirstTimestampMs } from "openclaw/plugin-sdk/number-runtime";
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry";
import type { PluginRuntime } from "openclaw/plugin-sdk/plugin-runtime";
import { withTimeout } from "openclaw/plugin-sdk/security-runtime";
@@ -12,7 +13,10 @@ import type {
SessionCatalogPullRequestSummary,
SessionCatalogTranscriptItem,
} from "openclaw/plugin-sdk/session-catalog";
import { asFiniteNumber, isRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
import {
isRecord,
normalizeBoundedOptionalString as readBoundedString,
} from "openclaw/plugin-sdk/string-coerce-runtime";
import { readClaudeDesktopCustomGroups } from "./claude-desktop-groups.js";
import { CLAUDE_CLI_BACKEND_ID, CLAUDE_CLI_DEFAULT_MODEL_REF } from "./cli-constants.js";
import {
@@ -222,14 +226,6 @@ function cacheCatalogDiscovery(filePath: string, entry: CatalogDiscoveryCacheEnt
setBoundedCache(catalogDiscoveryCache, filePath, entry, MAX_CATALOG_DISCOVERY_CACHE_ENTRIES);
}
function readBoundedString(value: unknown, maxLength = MAX_STRING_LENGTH): string | undefined {
if (typeof value !== "string") {
return undefined;
}
const trimmed = value.trim();
return trimmed && trimmed.length <= maxLength ? trimmed : undefined;
}
function pullRequestState(value: unknown): SessionCatalogPullRequestSummary["state"] | undefined {
if (typeof value !== "string") {
return undefined;
@@ -326,7 +322,7 @@ function isCliEntrypoint(value: unknown): value is string {
// Claude's persisted string timestamps are date expressions, including numeric-looking years.
// Numeric fields are already millisecond values, so preserve that distinct mixed-input contract.
function parseClaudeCatalogTimestampMs(value: unknown): number | undefined {
return typeof value === "string" ? asFiniteNumber(Date.parse(value)) : asFiniteNumber(value);
return parseDateFirstTimestampMs(value);
}
function isWithin(root: string, candidate: string): boolean {
@@ -644,7 +640,7 @@ async function readIndexRecords(context: ClaudeSessionScanContext): Promise<{
records.set(sessionId, {
threadId: sessionId,
name: summary ?? firstPrompt ?? null,
cwd: readBoundedString(entry.projectPath),
cwd: readBoundedString(entry.projectPath, MAX_STRING_LENGTH),
status: "stored",
...(createdAt !== undefined ? { createdAt } : {}),
...(updatedAt !== undefined ? { updatedAt, recencyAt: updatedAt } : {}),
@@ -822,7 +818,7 @@ async function discoverCliRecords(
records.set(sessionId, {
threadId: sessionId,
name: aiTitle ?? firstPrompt ?? null,
cwd: readBoundedString(raw.cwd),
cwd: readBoundedString(raw.cwd, MAX_STRING_LENGTH),
status: "stored",
...(createdAt !== undefined ? { createdAt } : {}),
updatedAt: stat.mtimeMs,
@@ -947,7 +943,9 @@ async function scanClaudeSessions(
}),
name: readBoundedString(metadata.title, 500) ?? existing?.name ?? null,
cwd:
readBoundedString(metadata.cwd) ?? readBoundedString(metadata.originCwd) ?? existing?.cwd,
readBoundedString(metadata.cwd, MAX_STRING_LENGTH) ??
readBoundedString(metadata.originCwd, MAX_STRING_LENGTH) ??
existing?.cwd,
...(createdAt !== undefined ? { createdAt } : {}),
...(updatedAt !== undefined ? { updatedAt, recencyAt: updatedAt } : {}),
...(customGroup ? { customGroup } : {}),
+2 -4
View File
@@ -178,10 +178,7 @@ function projectLiveModel(
const hasLiveFeatures = Array.isArray(row.supported_features);
const features = new Set(readStringArray(row.supported_features));
const pricing =
row.pricing && typeof row.pricing === "object" && !Array.isArray(row.pricing)
? (row.pricing as Record<string, unknown>)
: {};
const pricing = asNonArrayRecord(row.pricing);
const inputPrice = readPerTokenPrice(pricing.prompt);
const outputPrice = readPerTokenPrice(pricing.completion);
const cacheReadPrice = readPerTokenPrice(pricing.input_cache_read);
@@ -259,3 +256,4 @@ export function resolveBasetenDynamicModel(modelId: string) {
compat: buildBasetenModelCompat(id),
};
}
import { asNonArrayRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
+2 -6
View File
@@ -36,15 +36,11 @@ export function createBasetenThinkingWrapper(
if (!usesBasetenChatTemplateThinking(model.id)) {
return;
}
const existing =
payload.chat_template_args &&
typeof payload.chat_template_args === "object" &&
!Array.isArray(payload.chat_template_args)
? (payload.chat_template_args as Record<string, unknown>)
: {};
const existing = asNonArrayRecord(payload.chat_template_args);
payload.chat_template_args = {
...existing,
enable_thinking: isThinkingEnabled(ctx.thinkingLevel),
};
});
}
import { asNonArrayRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
+4 -9
View File
@@ -1,4 +1,7 @@
import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
import {
isRecord,
normalizeBoundedOptionalString as readBoundedString,
} from "openclaw/plugin-sdk/string-coerce-runtime";
export const BEAM_HOST_ID = "gateway";
export const BEAM_MAX_BODY_BYTES = 56 * 1024;
@@ -47,14 +50,6 @@ function hasOnlyKeys(value: Record<string, unknown>, allowed: Set<string>): bool
return Object.keys(value).every((key) => allowed.has(key));
}
function readBoundedString(value: unknown, maxLength: number): string | undefined {
if (typeof value !== "string") {
return undefined;
}
const trimmed = value.trim();
return trimmed && trimmed.length <= maxLength ? trimmed : undefined;
}
function isIsoTimestamp(value: string): boolean {
const match =
/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d{1,9})?(?:Z|([+-])(\d{2}):(\d{2}))$/.exec(
+6 -4
View File
@@ -229,13 +229,15 @@ vi.mock("./browser-tool.runtime.js", async () => {
const { wrapExternalContent } = await vi.importActual<typeof import("./sdk-security-runtime.js")>(
"./sdk-security-runtime.js",
);
const readStringValue = (value: unknown) => (typeof value === "string" ? value : undefined);
const readRawStringValue = (value: unknown) => (typeof value === "string" ? value : undefined);
const normalizeMockOptionalString = (value: unknown) =>
readRawStringValue(value)?.trim() || undefined;
const readStringParam = (
params: Record<string, unknown>,
key: string,
opts?: { required?: boolean; label?: string },
) => {
const value = readStringValue(params[key])?.trim();
const value = readRawStringValue(params[key])?.trim();
if (value) {
return value;
}
@@ -277,7 +279,7 @@ vi.mock("./browser-tool.runtime.js", async () => {
details: result,
}),
listNodes: nodesUtilsMocks.listNodes,
normalizeOptionalString: (value: unknown) => readStringValue(value)?.trim() || undefined,
normalizeOptionalString: normalizeMockOptionalString,
persistBrowserProxyFiles: vi.fn(async () => new Map<string, string>()),
readPositiveIntegerParam: (
params: Record<string, unknown>,
@@ -300,7 +302,7 @@ vi.mock("./browser-tool.runtime.js", async () => {
return value;
},
readStringParam,
readStringValue,
readStringValue: readRawStringValue,
resolveExistingUploadPaths: pathValidationMocks.resolveExistingUploadPaths,
resolveNodeIdFromList: (nodes: Array<Record<string, unknown>>, requested: string) => {
const node = nodes.find(
@@ -4,6 +4,7 @@
* Profile lookup, JSON errors, and route value coercion shared across browser
* control endpoints.
*/
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
import { BrowserProfileUnavailableError, type BrowserErrorResponse } from "../errors.js";
import {
type BrowserRouteContext,
@@ -13,10 +14,6 @@ import {
import { isProfileRestartRequiredError } from "../server-context.lifecycle.js";
import type { BrowserRequest, BrowserResponse } from "./types.js";
function normalizeOptionalString(value: string): string | undefined {
return value.trim() || undefined;
}
/**
* Extract profile name from query string or body and get profile context.
* Query string takes precedence over body for consistency with GET routes.
+2 -38
View File
@@ -1,7 +1,7 @@
/**
* Browser-local SDK config bridge plus Browser-specific default port helpers.
*/
import { normalizeOptionalLowercaseString } from "openclaw/plugin-sdk/string-coerce-runtime";
import { parseBooleanValue } from "openclaw/plugin-sdk/string-coerce-runtime";
export {
getRuntimeConfig,
@@ -64,41 +64,5 @@ export function deriveDefaultBrowserCdpPortRange(browserControlPort: number): Po
};
}
type BooleanParseOptions = {
truthy?: string[];
falsy?: string[];
};
const DEFAULT_TRUTHY = ["true", "1", "yes", "on"] as const;
const DEFAULT_FALSY = ["false", "0", "no", "off"] as const;
function matchesBooleanToken(value: string, tokens: readonly string[]): boolean {
return tokens.includes(value);
}
/** Parses common string booleans with optional custom truthy/falsy tokens. */
export function parseBooleanValue(
value: unknown,
options: BooleanParseOptions = {},
): boolean | undefined {
if (typeof value === "boolean") {
return value;
}
if (typeof value !== "string") {
return undefined;
}
const normalized = normalizeOptionalLowercaseString(value);
if (!normalized) {
return undefined;
}
const candidates: Array<[boolean, readonly string[]]> = [
[true, options.truthy ?? DEFAULT_TRUTHY],
[false, options.falsy ?? DEFAULT_FALSY],
];
for (const [parsed, tokens] of candidates) {
if (matchesBooleanToken(normalized, tokens)) {
return parsed;
}
}
return undefined;
}
export { parseBooleanValue };
+2 -4
View File
@@ -117,10 +117,7 @@ export default definePluginEntry({
defaultPlatforms: ["ios", "android", "macos", "windows", "linux", "unknown"],
foregroundRestrictedOnIos: true,
handle: async (ctx) => {
const params =
ctx.params && typeof ctx.params === "object" && !Array.isArray(ctx.params)
? (ctx.params as Record<string, unknown>)
: {};
const params = asNonArrayRecord(ctx.params);
// Native nodes also accept JSONL under `push` when messages[] is absent.
// Validate that fallback here so callers cannot bypass the JSONL policy.
const usesJsonl =
@@ -168,3 +165,4 @@ export default definePluginEntry({
);
},
});
import { asNonArrayRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
@@ -1,7 +1,10 @@
import { createHmac, randomBytes } from "node:crypto";
import { resolvePositiveTimerTimeoutMs } from "openclaw/plugin-sdk/number-runtime";
import { normalizeResolvedSecretInputString } from "openclaw/plugin-sdk/secret-input";
import { normalizeTrimmedStringList } from "openclaw/plugin-sdk/string-coerce-runtime";
import {
normalizeTrimmedStringList,
parseBooleanValue,
} from "openclaw/plugin-sdk/string-coerce-runtime";
import type { OpenClawExecAsk, OpenClawExecSecurity } from "./config-contracts.js";
import type { CodexServiceTier } from "./protocol.js";
@@ -73,17 +76,7 @@ export function normalizeStringList(value: unknown): string[] {
}
export function readBooleanEnv(value: string | undefined): boolean | undefined {
if (value === undefined) {
return undefined;
}
const normalized = value.trim().toLowerCase();
if (["1", "true", "yes", "on"].includes(normalized)) {
return true;
}
if (["0", "false", "no", "off"].includes(normalized)) {
return false;
}
return undefined;
return parseBooleanValue(value);
}
export function readExecSecurity(value: unknown): OpenClawExecSecurity | undefined {
@@ -9,6 +9,7 @@ import {
} from "openclaw/plugin-sdk/agent-harness-runtime";
import { openFileBackedSessionManagerForTest } from "openclaw/plugin-sdk/agent-runtime-test-contracts";
import { SessionManager } from "openclaw/plugin-sdk/agent-sessions";
import { toErrorObject as toLintErrorObject } from "openclaw/plugin-sdk/error-runtime";
import { initializeGlobalHookRunner } from "openclaw/plugin-sdk/hook-runtime";
import { MESSAGE_TOOL_DELIVERY_HINTS } from "openclaw/plugin-sdk/message-tool-delivery-hints";
import { createMockPluginRegistry } from "openclaw/plugin-sdk/plugin-test-runtime";
@@ -1961,17 +1962,4 @@ describe("runCodexAppServerAttempt context-engine lifecycle", () => {
});
});
function toLintErrorObject(value: unknown, fallbackMessage: string): Error {
if (value instanceof Error) {
return value;
}
if (typeof value === "string") {
return new Error(value);
}
const error = new Error(fallbackMessage, { cause: value });
if ((typeof value === "object" && value !== null) || typeof value === "function") {
Object.assign(error, value);
}
return error;
}
/* oxlint-disable max-lines -- TODO: split this grandfathered oversized file. */
@@ -1,3 +1,4 @@
import { normalizeBoundedOptionalString as readBoundedId } from "openclaw/plugin-sdk/string-coerce-runtime";
import { isJsonObject, type CodexThreadListParams } from "./protocol.js";
import type { CodexAppServerBindingStore } from "./session-binding.js";
@@ -6,14 +7,6 @@ const MAX_DESCENDANT_PAGES = 100;
const MAX_THREAD_ID_LENGTH = 256;
const MAX_CURSOR_LENGTH = 4096;
function readBoundedId(value: unknown): string | undefined {
if (typeof value !== "string") {
return undefined;
}
const normalized = value.trim();
return normalized && normalized.length <= MAX_THREAD_ID_LENGTH ? normalized : undefined;
}
function readNextCursor(value: unknown): string | undefined {
if (value === undefined || value === null) {
return undefined;
@@ -34,7 +27,7 @@ export async function assertCodexArchiveDescendantsUnowned(params: {
listPage: (request: CodexThreadListParams) => Promise<unknown>;
assertDescendantIdle: (threadId: string) => Promise<void>;
}): Promise<void> {
const ancestorThreadId = readBoundedId(params.threadId);
const ancestorThreadId = readBoundedId(params.threadId, MAX_THREAD_ID_LENGTH);
if (!ancestorThreadId) {
throw new Error("cannot verify Codex archive descendants for an invalid thread id");
}
@@ -64,7 +57,7 @@ export async function assertCodexArchiveDescendantsUnowned(params: {
if (!isJsonObject(value)) {
throw new Error("Codex app-server returned an invalid descendant thread");
}
const descendantThreadId = readBoundedId(value.id);
const descendantThreadId = readBoundedId(value.id, MAX_THREAD_ID_LENGTH);
if (!descendantThreadId) {
throw new Error("Codex app-server returned a descendant without a valid thread id");
}
@@ -1,4 +1,5 @@
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
import type { JsonObject } from "./protocol.js";
export type CodexWebSearchPlan = {
@@ -14,10 +15,6 @@ const CODEX_NATIVE_WEB_SEARCH_DISABLED_CONFIG: JsonObject = {
web_search: "disabled",
};
function normalizeOptionalString(value: unknown): string | undefined {
return typeof value === "string" ? value.trim() || undefined : undefined;
}
function normalizeUniqueStrings(value: unknown): string[] | undefined {
if (!Array.isArray(value)) {
return undefined;
+2 -3
View File
@@ -6,6 +6,7 @@ import {
resolveUserPath,
TRANSCRIPT_CREDENTIAL_SAFETY_PROMPT,
} from "openclaw/plugin-sdk/agent-harness-runtime";
import { readNonEmptyStringPreservingWhitespace as readNonEmptyString } from "openclaw/plugin-sdk/string-coerce-runtime";
import {
COPILOT_ASK_USER_AVAILABLE_TOOLS,
COPILOT_SETTLED_FINALIZATION_SYSTEM_MESSAGE,
@@ -334,9 +335,7 @@ export function createSystemMessageContent(
export function isRawCopilotModelRun(params: AttemptParamsLike): boolean {
return params.modelRun === true || params.promptMode === "none";
}
export function readNonEmptyString(value: unknown): string | undefined {
return typeof value === "string" && value.length > 0 ? value : undefined;
}
export { readNonEmptyString };
export function readResolvedAttemptPath(value: unknown): string | undefined {
const raw = readNonEmptyString(value)?.trim();
if (!raw) {
+1 -13
View File
@@ -14,6 +14,7 @@ import {
type AgentMessage,
} from "openclaw/plugin-sdk/agent-harness-runtime";
import type { SandboxContext } from "openclaw/plugin-sdk/agent-harness-runtime";
import { toErrorObject as toLintErrorObject } from "openclaw/plugin-sdk/error-runtime";
import { createDeferred } from "openclaw/plugin-sdk/extension-shared";
import {
initializeGlobalHookRunner,
@@ -4903,17 +4904,4 @@ describe("runCopilotAttempt", () => {
});
});
function toLintErrorObject(value: unknown, fallbackMessage: string): Error {
if (value instanceof Error) {
return value;
}
if (typeof value === "string") {
return new Error(value);
}
const error = new Error(fallbackMessage, { cause: value });
if ((typeof value === "object" && value !== null) || typeof value === "function") {
Object.assign(error, value);
}
return error;
}
/* oxlint-disable max-lines -- TODO: split this grandfathered oversized file. */
+1 -4
View File
@@ -2,6 +2,7 @@
import { createHash } from "node:crypto";
import { homedir as osHomedir } from "node:os";
import { join, resolve } from "node:path";
import { readNonEmptyStringPreservingWhitespace as readNonEmptyString } from "openclaw/plugin-sdk/string-coerce-runtime";
/**
* Pure functional auth resolver for the copilot agent runtime.
@@ -331,7 +332,3 @@ export function tokenFingerprint(token: string): string {
const hex = createHash("sha256").update(token).digest("hex").slice(0, 12);
return `sha256:${hex}`;
}
function readNonEmptyString(value: unknown): string | undefined {
return typeof value === "string" && value.length > 0 ? value : undefined;
}
@@ -1,6 +1,8 @@
import type { Attachment, SessionEvent } from "@github/copilot-sdk";
import type { AgentMessage } from "openclaw/plugin-sdk/agent-harness-runtime";
import { sanitizeToolResult } from "openclaw/plugin-sdk/agent-harness-runtime";
import { parseDateStringTimestampMs } from "openclaw/plugin-sdk/number-runtime";
import { readNonEmptyStringPreservingWhitespace as readNonEmptyString } from "openclaw/plugin-sdk/string-coerce-runtime";
import { buildCopilotAssistantUsage, type CopilotUsageSnapshot } from "./usage-bridge.js";
export type AssistantMessage = Extract<AgentMessage, { role: "assistant" }>;
@@ -83,8 +85,7 @@ export function resolveAssistantUsage(
}
export function resolveEventTimestamp(timestamp: string, now: () => number): number {
const parsed = Date.parse(timestamp);
return Number.isFinite(parsed) ? parsed : now();
return parseDateStringTimestampMs(timestamp) ?? now();
}
export function hasOwnKeys(value: unknown): value is Record<string, unknown> {
@@ -161,7 +162,3 @@ export function sanitizeToolDetailText(text: string): string {
const value = sanitized.content?.[0]?.text;
return typeof value === "string" ? value : "";
}
function readNonEmptyString(value: unknown): string | undefined {
return typeof value === "string" && value.length > 0 ? value : undefined;
}
+1 -4
View File
@@ -5,6 +5,7 @@ import type {
AgentMessage,
} from "openclaw/plugin-sdk/agent-harness-runtime";
import { toErrorObject } from "openclaw/plugin-sdk/error-runtime";
import { readNonEmptyStringPreservingWhitespace as readNonEmptyString } from "openclaw/plugin-sdk/string-coerce-runtime";
import {
buildAssistantMessage,
hasOwnKeys,
@@ -969,10 +970,6 @@ function splitPlanText(text: string | undefined): string[] {
.filter((line) => line.length > 0);
}
function readNonEmptyString(value: unknown): string | undefined {
return typeof value === "string" && value.length > 0 ? value : undefined;
}
function registerListener<K extends SessionEventType>(
session: SessionLike,
unsubscribeFns: Array<() => void>,
+1 -14
View File
@@ -1,6 +1,7 @@
// Copilot tests cover runtime plugin behavior.
import { normalize, resolve, sep } from "node:path";
import type { CopilotClient, CopilotClientOptions } from "@github/copilot-sdk";
import { toErrorObject as toLintErrorObject } from "openclaw/plugin-sdk/error-runtime";
import { createDeferred } from "openclaw/plugin-sdk/extension-shared";
import { afterEach, describe, expect, it, vi } from "vitest";
import type { ClientCreateOptions, PoolKey } from "./runtime.js";
@@ -483,17 +484,3 @@ describe("createCopilotClientPool", () => {
expect(String(sdk.ctorCalls[0]?.baseDirectory)).toBe(normalizedHome);
});
});
function toLintErrorObject(value: unknown, fallbackMessage: string): Error {
if (value instanceof Error) {
return value;
}
if (typeof value === "string") {
return new Error(value);
}
const error = new Error(fallbackMessage, { cause: value });
if ((typeof value === "object" && value !== null) || typeof value === "function") {
Object.assign(error, value);
}
return error;
}
@@ -8,6 +8,7 @@ import {
resolveBootstrapContextForRun,
resolveUserPath,
} from "openclaw/plugin-sdk/agent-harness-runtime";
import { hasNonEmptyString } from "openclaw/plugin-sdk/string-coerce-runtime";
// Filenames the Copilot SDK already loads natively from the working
// directory / instructionDirectories (per
@@ -235,7 +236,7 @@ function getCopilotContextFileBasename(filePath: string): string {
}
function readNonEmptyString(value: unknown): string | undefined {
return typeof value === "string" && value.trim().length > 0 ? value : undefined;
return hasNonEmptyString(value) ? value : undefined;
}
function readResolvedWorkspacePath(value: unknown): string | undefined {
@@ -1,6 +1,7 @@
import { readFileSync } from "node:fs";
import nodePath from "node:path";
import { normalizeDiagnosticValue } from "openclaw/plugin-sdk/diagnostic-runtime";
import { collectErrorGraphCandidates } from "openclaw/plugin-sdk/error-runtime";
import { createNodeProxyAgent } from "openclaw/plugin-sdk/fetch-runtime";
import {
OTEL_EXPORTER_OTLP_CERTIFICATE_ENV,
@@ -199,49 +200,6 @@ export function errorCategory(err: unknown): string {
}
}
function collectNestedErrorCandidates(err: unknown): unknown[] {
const queue: unknown[] = [err];
const seen = new Set<unknown>();
const candidates: unknown[] = [];
while (queue.length > 0) {
const current = queue.shift();
if (current == null || seen.has(current)) {
continue;
}
seen.add(current);
candidates.push(current);
if (Array.isArray(current)) {
for (const item of current) {
if (item != null && !seen.has(item)) {
queue.push(item);
}
}
continue;
}
if (typeof current !== "object") {
continue;
}
const record = current as Record<string, unknown>;
for (const nested of [record.cause, record.reason, record.original, record.error]) {
if (nested != null && !seen.has(nested)) {
queue.push(nested);
}
}
if (Array.isArray(record.errors)) {
for (const nested of record.errors) {
if (nested != null && !seen.has(nested)) {
queue.push(nested);
}
}
}
}
return candidates;
}
function readErrorName(err: unknown): string | undefined {
if (!err || typeof err !== "object") {
return undefined;
@@ -259,7 +217,17 @@ export function readErrorCode(err: unknown): string | number | undefined {
}
export function findOtlpExporterError(reason: unknown): object | undefined {
for (const candidate of collectNestedErrorCandidates(reason)) {
for (const candidate of collectErrorGraphCandidates(reason, (current) =>
Array.isArray(current)
? current
: [
current.cause,
current.reason,
current.original,
current.error,
...(Array.isArray(current.errors) ? current.errors : []),
],
)) {
if (
readErrorName(candidate) === "OTLPExporterError" &&
candidate &&
+4 -3
View File
@@ -22,19 +22,20 @@ const BASE_LANGUAGE_ALIASES = new Map<string, SupportedLanguages>(
);
type DiffPayloadFile = FileContents | FileDiffMetadata;
function normalizeOptionalString(value: unknown): string | undefined {
// The curated viewer bundles this module outside Plugin SDK package resolution.
function normalizeLanguageHint(value: unknown): string | undefined {
if (typeof value !== "string") {
return undefined;
}
const normalized = value.trim().toLowerCase();
return normalized ? normalized : undefined;
return normalized || undefined;
}
export async function normalizeSupportedLanguageHint(
value?: string,
options: { languagePackAvailable?: boolean } = {},
): Promise<SupportedLanguages | undefined> {
const normalized = normalizeOptionalString(value);
const normalized = normalizeLanguageHint(value);
if (!normalized) {
return undefined;
}
+2 -5
View File
@@ -38,9 +38,6 @@ export function formatDiscordUserTag(user: User) {
}
export function resolveTimestampMs(timestamp?: string | null) {
if (!timestamp) {
return undefined;
}
const parsed = Date.parse(timestamp);
return Number.isNaN(parsed) ? undefined : parsed;
return parseDateStringTimestampMs(timestamp);
}
import { parseDateStringTimestampMs } from "openclaw/plugin-sdk/number-runtime";
@@ -33,15 +33,16 @@ vi.mock("openclaw/plugin-sdk/runtime-env", () => ({
danger: (value: string) => value,
}));
vi.mock("openclaw/plugin-sdk/string-coerce-runtime", () => ({
normalizeOptionalString: (value: string | null | undefined) => {
vi.mock("openclaw/plugin-sdk/string-coerce-runtime", () => {
const normalizeMockOptionalString = (value: string | null | undefined) => {
if (typeof value !== "string") {
return undefined;
}
const normalized = value.trim();
return normalized.length > 0 ? normalized : undefined;
},
}));
};
return { normalizeOptionalString: normalizeMockOptionalString };
});
vi.mock("../proxy-request-client.js", () => ({
DISCORD_REST_TIMEOUT_MS: 15_000,
+11 -7
View File
@@ -1,4 +1,4 @@
import { asOptionalObjectRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
import { asRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
// Feishu helper module supports card test helpers behavior.
import { expect } from "vitest";
@@ -10,6 +10,10 @@ function asArray(value: unknown): unknown[] {
return Array.isArray(value) ? value : [];
}
function readFeishuObjectRecord(value: unknown): Record<string, unknown> | undefined {
return value !== null && typeof value === "object" ? asRecord(value) : undefined;
}
export function expectFirstSentCardUsesFillWidthOnly(sendCardMock: {
mock: { calls: unknown[][] };
}) {
@@ -33,17 +37,17 @@ export function expectFirstSentCardUsesFillWidthOnly(sendCardMock: {
export function expectSentCardHasP2pAction(sendCardMock: MockCalls) {
const hasP2pAction = sendCardMock.mock.calls.some(([arg]) => {
const card = asOptionalObjectRecord(asOptionalObjectRecord(arg)?.card);
const body = asOptionalObjectRecord(card?.body);
const card = readFeishuObjectRecord(readFeishuObjectRecord(arg)?.card);
const body = readFeishuObjectRecord(card?.body);
return asArray(body?.elements).some((element) => {
const elementRecord = asOptionalObjectRecord(element);
const elementRecord = readFeishuObjectRecord(element);
if (elementRecord?.tag !== "action") {
return false;
}
return asArray(elementRecord.actions).some((action) => {
const actionRecord = asOptionalObjectRecord(action);
const value = asOptionalObjectRecord(actionRecord?.value);
const command = asOptionalObjectRecord(value?.c);
const actionRecord = readFeishuObjectRecord(action);
const value = readFeishuObjectRecord(actionRecord?.value);
const command = readFeishuObjectRecord(value?.c);
return command?.t === "p2p";
});
});
@@ -2,7 +2,7 @@
import crypto from "node:crypto";
import type { AnyAgentTool } from "openclaw/plugin-sdk/agent-harness-runtime";
import { readMediaBuffer } from "openclaw/plugin-sdk/media-store";
import { asBoolean } from "openclaw/plugin-sdk/string-coerce-runtime";
import { asBoolean, asNonArrayRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
import { appendFileTransferAudit } from "../shared/audit.js";
import { inspectStrictBase64 } from "../shared/base64.js";
import { humanSize } from "../shared/params.js";
@@ -66,10 +66,7 @@ export function createFileWriteTool(): AnyAgentTool {
return {
...FILE_WRITE_TOOL_DESCRIPTOR,
async execute(_toolCallId, params) {
const raw: Record<string, unknown> =
params && typeof params === "object" && !Array.isArray(params)
? (params as Record<string, unknown>)
: {};
const raw = asNonArrayRecord(params);
const { node: nodeQuery, requestedPath: filePath } = readRequiredNodePath(raw);
const contentBase64 = typeof raw.contentBase64 === "string" ? raw.contentBase64 : undefined;
+1 -8
View File
@@ -1,12 +1,5 @@
// Google Meet plugin module implements shared Meet URL contracts.
function normalizeOptionalString(value: unknown): string | undefined {
if (typeof value !== "string") {
return undefined;
}
const trimmed = value.trim();
return trimmed || undefined;
}
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
export function normalizeMeetUrl(input: unknown): string {
const raw = normalizeOptionalString(input);
+1 -8
View File
@@ -87,14 +87,6 @@ async function attachDocumentText<T extends { docsDestination?: Record<string, u
}
}
function parseGoogleMeetTimestamp(value: string | undefined): number | undefined {
if (!value?.trim()) {
return undefined;
}
const parsed = Date.parse(value);
return Number.isFinite(parsed) ? parsed : undefined;
}
function isoFromMs(value: number | undefined): string | undefined {
return typeof value === "number" && Number.isFinite(value)
? new Date(value).toISOString()
@@ -414,3 +406,4 @@ export function buildGoogleMeetPreflightReport(params: {
blockers,
};
}
import { parseDateStringTimestampMs as parseGoogleMeetTimestamp } from "openclaw/plugin-sdk/number-runtime";
+1 -14
View File
@@ -1,4 +1,5 @@
// Google tests cover google plugin behavior.
import { toErrorObject as toLintErrorObject } from "openclaw/plugin-sdk/error-runtime";
import { completeSimple, type Model } from "openclaw/plugin-sdk/llm";
import { resolveFfmpegBin } from "openclaw/plugin-sdk/media-runtime";
import {
@@ -449,17 +450,3 @@ describeLive("google plugin live", () => {
});
}, 120_000);
});
function toLintErrorObject(value: unknown, fallbackMessage: string): Error {
if (value instanceof Error) {
return value;
}
if (typeof value === "string") {
return new Error(value);
}
const error = new Error(fallbackMessage, { cause: value });
if ((typeof value === "object" && value !== null) || typeof value === "function") {
Object.assign(error, value);
}
return error;
}
+1 -13
View File
@@ -4,6 +4,7 @@ import os from "node:os";
import path from "node:path";
import { gzipSync } from "node:zlib";
import { expectDefined } from "@openclaw/normalization-core";
import { toErrorObject as toLintErrorObject } from "openclaw/plugin-sdk/error-runtime";
import type { Model } from "openclaw/plugin-sdk/llm";
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { resetGoogleVertexAdcState } from "./google-oauth.test-support.js";
@@ -2894,17 +2895,4 @@ describe("google transport stream", () => {
});
});
function toLintErrorObject(value: unknown, fallbackMessage: string): Error {
if (value instanceof Error) {
return value;
}
if (typeof value === "string") {
return new Error(value);
}
const error = new Error(fallbackMessage, { cause: value });
if ((typeof value === "object" && value !== null) || typeof value === "function") {
Object.assign(error, value);
}
return error;
}
/* oxlint-disable max-lines -- TODO: split this grandfathered oversized file. */
+1 -8
View File
@@ -67,14 +67,6 @@ function normalizeAudienceType(value?: string | null): GoogleChatAudienceType |
return undefined;
}
function resolveGoogleChatTimestampMs(eventTime?: string): number | undefined {
if (!eventTime) {
return undefined;
}
const parsed = Date.parse(eventTime);
return Number.isFinite(parsed) ? parsed : undefined;
}
function resolveGoogleChatBotLoopProtection(params: {
allowBots: boolean;
isBotSender: boolean;
@@ -588,3 +580,4 @@ export function resolveGoogleChatWebhookPath(params: {
defaultPath: "/googlechat",
});
}
import { parseDateStringTimestampMs as resolveGoogleChatTimestampMs } from "openclaw/plugin-sdk/number-runtime";
+2 -4
View File
@@ -57,10 +57,7 @@ function cleanToken(value: unknown, fallback: string): string {
}
function buildActionMessage(action: unknown, sessionKey?: string): string {
const value =
action && typeof action === "object" && !Array.isArray(action)
? (action as Record<string, unknown>)
: {};
const value = asNonArrayRecord(action);
const actionName = cleanToken(value.name, "unknown");
const surface = cleanToken(value.surfaceId, "main");
const component = cleanToken(value.sourceComponentId, "unknown");
@@ -188,3 +185,4 @@ export function createLinuxCanvasCommands(
return registration;
});
}
import { asNonArrayRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
+2 -3
View File
@@ -1,5 +1,6 @@
import type { OpenClawPluginNodeHostCommandAvailabilityContext } from "openclaw/plugin-sdk/plugin-entry";
import type { CommandOptions, SpawnResult } from "openclaw/plugin-sdk/process-runtime";
import { asNonArrayRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
import {
resolveLinuxNodePluginConfigFromHost,
@@ -14,9 +15,7 @@ export function parseParams(paramsJSON: string | null | undefined): Record<strin
}
try {
const parsed = JSON.parse(paramsJSON) as unknown;
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
? (parsed as Record<string, unknown>)
: {};
return asNonArrayRecord(parsed);
} catch {
return {};
}
@@ -14,6 +14,7 @@ import {
type MemoryEmbeddingProviderCreateOptions,
type MemoryEmbeddingProviderCreateResult,
} from "openclaw/plugin-sdk/memory-core-host-engine-embeddings";
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
import { formatLlamaCppSetupError, resolveNodeLlamaCppImportUrl } from "./node-llama.runtime.js";
type LlamaCppLocalOptions = {
@@ -42,10 +43,6 @@ type LlamaCppModelIdentity = {
}>;
};
function normalizeOptionalString(value: unknown): string | undefined {
return typeof value === "string" && value.trim() ? value.trim() : undefined;
}
function readLocalOptions(options: { local?: unknown }): LlamaCppLocalOptions {
const local = options.local as LlamaCppLocalOptions | undefined;
return local ?? {};
@@ -82,9 +82,7 @@ function extractText(content: unknown): string {
}
function normalizeArguments(value: unknown): Record<string, unknown> {
return value && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: {};
return asNonArrayRecord(value);
}
async function resolveLlamaCppResponseGrammar(params: {
@@ -710,3 +708,4 @@ if (process.env.VITEST || process.env.NODE_ENV === "test") {
mapToolsToLlamaFunctions,
});
}
import { asNonArrayRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
+1 -14
View File
@@ -2,6 +2,7 @@ import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
// Lobster tests cover lobster runner plugin behavior.
import { toErrorObject as toLintErrorObject } from "openclaw/plugin-sdk/error-runtime";
import { createRequireRecord } from "openclaw/plugin-sdk/test-fixtures";
import { afterEach, describe, expect, it, vi } from "vitest";
import { createEmbeddedLobsterRunner, resolveLobsterCwd } from "./lobster-runner.js";
@@ -584,17 +585,3 @@ describe("createEmbeddedLobsterRunner", () => {
).rejects.toThrow(/timed out|aborted/);
});
});
function toLintErrorObject(value: unknown, fallbackMessage: string): Error {
if (value instanceof Error) {
return value;
}
if (typeof value === "string") {
return new Error(value);
}
const error = new Error(fallbackMessage, { cause: value });
if ((typeof value === "object" && value !== null) || typeof value === "function") {
Object.assign(error, value);
}
return error;
}
+1 -14
View File
@@ -2,6 +2,7 @@
import { stat } from "node:fs/promises";
import path from "node:path";
import { Readable, Writable } from "node:stream";
import { toErrorObject as toLintErrorObject } from "openclaw/plugin-sdk/error-runtime";
export type LobsterEnvelope =
| {
@@ -281,17 +282,3 @@ export function createEmbeddedLobsterRunner(options?: {
},
};
}
function toLintErrorObject(value: unknown, fallbackMessage: string): Error {
if (value instanceof Error) {
return value;
}
if (typeof value === "string") {
return new Error(value);
}
const error = new Error(fallbackMessage, { cause: value });
if ((typeof value === "object" && value !== null) || typeof value === "function") {
Object.assign(error, value);
}
return error;
}
@@ -17,7 +17,7 @@ import { LogService } from "./logger.js";
import { isRepairableSecretStorageAccessError } from "./recovery-key-store.js";
import type { MatrixCryptoBootstrapApi, MatrixDeviceVerificationStatusLike } from "./types.js";
const normalizeOptionalString = normalizeNullableString;
const normalizeNullableVerificationString = normalizeNullableString;
export abstract class MatrixClientVerification extends MatrixClientCore {
async getRoomKeyBackupStatus(): Promise<MatrixRoomKeyBackupStatus> {
@@ -218,7 +218,7 @@ export abstract class MatrixClientVerification extends MatrixClientCore {
return null;
}
const version = await crypto.getActiveSessionBackupVersion().catch(() => null);
return normalizeOptionalString(version);
return normalizeNullableVerificationString(version);
}
protected async resolveCachedRoomKeyBackupDecryptionKey(
@@ -275,7 +275,7 @@ export abstract class MatrixClientVerification extends MatrixClientCore {
let matchesDecryptionKey: boolean | null = null;
if (typeof crypto.getKeyBackupInfo === "function") {
const info = await crypto.getKeyBackupInfo().catch(() => null);
serverVersion = normalizeOptionalString(info?.version) ?? serverVersion;
serverVersion = normalizeNullableVerificationString(info?.version) ?? serverVersion;
if (info && typeof crypto.isKeyBackupTrusted === "function") {
const trustInfo = await crypto.isKeyBackupTrusted(info).catch(() => null);
trusted = typeof trustInfo?.trusted === "boolean" ? trustInfo.trusted : null;
@@ -304,7 +304,7 @@ export abstract class MatrixClientVerification extends MatrixClientCore {
const response = (await this.doRequest("GET", "/_matrix/client/v3/room_keys/version")) as {
version?: string;
};
return normalizeOptionalString(response.version);
return normalizeNullableVerificationString(response.version);
} catch {
return null;
}
+6 -34
View File
@@ -1,5 +1,6 @@
// Mattermost plugin module implements client behavior.
import { createChannelPartialDeliveryError } from "openclaw/plugin-sdk/channel-inbound";
import { collectErrorGraphCandidates } from "openclaw/plugin-sdk/error-runtime";
import { buildTimeoutAbortSignal } from "openclaw/plugin-sdk/extension-shared";
import { responseWithRelease } from "openclaw/plugin-sdk/fetch-runtime";
import { resolveTimerTimeoutMs } from "openclaw/plugin-sdk/number-runtime";
@@ -513,7 +514,11 @@ export async function createMattermostDirectChannelWithRetry(
}
function isRetryableError(error: Error): boolean {
const candidates = collectErrorCandidates(error);
const candidates = collectErrorGraphCandidates(error, (current) => [
current.cause,
current.reason,
...(Array.isArray(current.errors) ? current.errors : []),
]);
const messages = candidates
.map((candidate) => normalizeLowercaseStringOrEmpty(readErrorMessage(candidate)))
.filter((message): message is string => Boolean(message));
@@ -592,39 +597,6 @@ function isRetryableError(error: Error): boolean {
);
}
function collectErrorCandidates(error: unknown): unknown[] {
const queue: unknown[] = [error];
let queueIndex = 0;
const seen = new Set<unknown>();
const candidates: unknown[] = [];
while (queueIndex < queue.length) {
const current = queue[queueIndex];
queueIndex += 1;
if (!current || seen.has(current)) {
continue;
}
seen.add(current);
candidates.push(current);
if (typeof current !== "object") {
continue;
}
const nested = current as {
cause?: unknown;
reason?: unknown;
errors?: unknown;
};
queue.push(nested.cause, nested.reason);
if (Array.isArray(nested.errors)) {
queue.push(...nested.errors);
}
}
return candidates;
}
function readErrorMessage(error: unknown): string | undefined {
if (!error || typeof error !== "object") {
return undefined;
@@ -11,7 +11,6 @@ import {
legacyStateFileExists,
type PluginDoctorStateMigration,
} from "openclaw/plugin-sdk/runtime-doctor-migrations";
import { asOptionalObjectRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
// sqlite-runtime re-exports the agent-db/kysely graph; keep it lazy so doctor
// enumeration does not cold-load it with this closure.
import {
@@ -27,12 +26,19 @@ function formatLegacyVectorRows(count: number | undefined): string {
type MemoryFtsTokenizer = "unicode61" | "trigram";
// This doctor closure must stay dependency-light while accepting legacy array-backed objects.
function readLegacyObjectRecord(value: unknown): Record<string, unknown> | undefined {
return value !== null && typeof value === "object"
? (value as Record<string, unknown>)
: undefined;
}
function resolveConfiguredAgentIds(config: unknown): string[] {
const cfg = config as { agents?: { entries?: unknown; list?: unknown } };
const entries = asOptionalObjectRecord(cfg.agents?.entries);
const entries = readLegacyObjectRecord(cfg.agents?.entries);
const listedIds = Array.isArray(cfg.agents?.list)
? cfg.agents.list.flatMap((entry) => {
const id = asOptionalObjectRecord(entry)?.id;
const id = readLegacyObjectRecord(entry)?.id;
return typeof id === "string" ? [id] : [];
})
: [];
@@ -44,40 +50,40 @@ function readAgentMemorySearch(
config: unknown,
agentId: string,
): Record<string, unknown> | undefined {
const agents = asOptionalObjectRecord(asOptionalObjectRecord(config)?.agents);
const keyedEntries = asOptionalObjectRecord(agents?.entries);
const agents = readLegacyObjectRecord(readLegacyObjectRecord(config)?.agents);
const keyedEntries = readLegacyObjectRecord(agents?.entries);
const keyedEntry = keyedEntries
? Object.entries(keyedEntries).find(([id]) => normalizeAgentId(id) === agentId)?.[1]
: undefined;
const keyedSearch = asOptionalObjectRecord(
asOptionalObjectRecord(asOptionalObjectRecord(keyedEntry)?.memory)?.search,
const keyedSearch = readLegacyObjectRecord(
readLegacyObjectRecord(readLegacyObjectRecord(keyedEntry)?.memory)?.search,
);
if (keyedSearch) {
return keyedSearch;
}
const entries = Array.isArray(agents?.list) ? agents.list : [];
const entry = entries
.map(asOptionalObjectRecord)
.map(readLegacyObjectRecord)
.find(
(candidate) =>
normalizeAgentId(typeof candidate?.id === "string" ? candidate.id : undefined) === agentId,
);
return asOptionalObjectRecord(asOptionalObjectRecord(entry?.memory)?.search);
return readLegacyObjectRecord(readLegacyObjectRecord(entry?.memory)?.search);
}
function readMemorySearchLayers(config: unknown, agentId: string): Record<string, unknown>[] {
const cfg = asOptionalObjectRecord(config);
const cfg = readLegacyObjectRecord(config);
return [
readAgentMemorySearch(config, agentId),
asOptionalObjectRecord(asOptionalObjectRecord(cfg?.memory)?.search),
readLegacyObjectRecord(readLegacyObjectRecord(cfg?.memory)?.search),
// Doctor still inspects the retired root shape to migrate its persisted sidecar path.
asOptionalObjectRecord(cfg?.memorySearch),
readLegacyObjectRecord(cfg?.memorySearch),
].filter((value): value is Record<string, unknown> => value !== undefined);
}
function readStoreLayers(config: unknown, agentId: string): Record<string, unknown>[] {
return readMemorySearchLayers(config, agentId).flatMap((search) => {
const store = asOptionalObjectRecord(search.store);
const store = readLegacyObjectRecord(search.store);
return store ? [store] : [];
});
}
@@ -92,7 +98,7 @@ function readNestedStoreLayers(
key: string,
): Record<string, unknown>[] {
return readStoreLayers(config, agentId).flatMap((store) => {
const nested = asOptionalObjectRecord(store[key]);
const nested = readLegacyObjectRecord(store[key]);
return nested ? [nested] : [];
});
}
@@ -441,11 +441,7 @@ export function normalizeShortTermRecallStore(raw: unknown, nowIso: string): Sho
}
export function parseStoreTimestampMs(value: string | undefined): number {
if (!value) {
return Number.NEGATIVE_INFINITY;
}
const parsed = Date.parse(value);
return Number.isFinite(parsed) ? parsed : Number.NEGATIVE_INFINITY;
return parseDateStringTimestampMs(value) ?? Number.NEGATIVE_INFINITY;
}
export function compareStoreTimestampDesc(
@@ -606,3 +602,4 @@ export function parseEntryRangeFromKey(
}
return { startLine: 1, endLine: 1 };
}
import { parseDateStringTimestampMs } from "openclaw/plugin-sdk/number-runtime";
+2 -5
View File
@@ -50,11 +50,7 @@ export type WikiPageContradictionCluster = {
};
function parseTimestamp(value?: string): number | null {
if (!value?.trim()) {
return null;
}
const parsed = Date.parse(value);
return Number.isFinite(parsed) ? parsed : null;
return parseDateStringTimestampMs(value) ?? null;
}
function clampDaysSinceTouch(daysSinceTouch: number): number {
@@ -251,3 +247,4 @@ export function buildPageContradictionClusters(
}))
.toSorted((left, right) => left.label.localeCompare(right.label));
}
import { parseDateStringTimestampMs } from "openclaw/plugin-sdk/number-runtime";
+1 -8
View File
@@ -1,5 +1,6 @@
// Hermes-native auth discovery and reauthentication planning.
import { createMigrationManualItem } from "openclaw/plugin-sdk/migration";
import { parseDateStringTimestampMs as readTimestamp } from "openclaw/plugin-sdk/number-runtime";
import type { MigrationItem } from "openclaw/plugin-sdk/plugin-entry";
import { isRecord, normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
import { readText } from "./helpers.js";
@@ -32,14 +33,6 @@ const HERMES_REAUTH_SOURCE_PROVIDERS = new Set<string>(
HERMES_REAUTH_PROVIDER_MAPPINGS.map((entry) => entry.sourceProvider),
);
function readTimestamp(value: unknown): number | undefined {
if (typeof value !== "string" || !value.trim()) {
return undefined;
}
const parsed = Date.parse(value);
return Number.isFinite(parsed) ? parsed : undefined;
}
function readHermesProviderCandidate(
auth: Record<string, unknown>,
sourcePath: string,
@@ -1,4 +1,5 @@
// Hermes provider config contract parsing and normalization.
import { asPositiveFiniteNumber as readPositiveNumber } from "openclaw/plugin-sdk/number-runtime";
import { isRecord, normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
import {
MCP_ENV_REFERENCE_RE,
@@ -163,9 +164,7 @@ export function resolveHermesImplicitBaseUrl(providerId: string | undefined): st
: undefined;
}
export function readPositiveNumber(value: unknown): number | undefined {
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : undefined;
}
export { readPositiveNumber };
export function resolveProviderApi(
raw: Record<string, unknown>,
+2 -2
View File
@@ -10,7 +10,7 @@ import {
} from "openclaw/plugin-sdk/migration";
import type { MigrationItem } from "openclaw/plugin-sdk/plugin-entry";
import { appendRegularFile, pathExists } from "openclaw/plugin-sdk/security-runtime";
import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
import { asNonArrayRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
import { parse as parseYaml } from "yaml";
const HOME_SHORTHAND_RE = /^~(?=$|[\\/])/u;
@@ -59,7 +59,7 @@ export function childRecord(
key: string,
): Record<string, unknown> {
const value = root?.[key];
return isRecord(value) ? value : {};
return asNonArrayRecord(value);
}
export function readStringArray(value: unknown): string[] {
+1 -14
View File
@@ -1,4 +1,5 @@
// Moonshot tests cover moonshot plugin behavior.
import { toErrorObject as toLintErrorObject } from "openclaw/plugin-sdk/error-runtime";
import {
streamSimple,
type AssistantMessage,
@@ -438,17 +439,3 @@ describeModelLive("moonshot K3 live", () => {
throw toLintErrorObject(lastAuthError, "Moonshot K3 rejected the API key in both regions");
}, 180_000);
});
function toLintErrorObject(value: unknown, fallbackMessage: string): Error {
if (value instanceof Error) {
return value;
}
if (typeof value === "string") {
return new Error(value);
}
const error = new Error(fallbackMessage, { cause: value });
if ((typeof value === "object" && value !== null) || typeof value === "function") {
Object.assign(error, value);
}
return error;
}
@@ -4,6 +4,8 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
// Mock shared.js to avoid transitive runtime-api imports that pull in uninstalled packages.
vi.mock("./shared.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("./shared.js")>();
const isMockRecord = (value: unknown) =>
typeof value === "object" && value !== null && !Array.isArray(value);
return {
...actual,
applyAuthorizationHeaderForUrl: vi.fn(),
@@ -11,7 +13,7 @@ vi.mock("./shared.js", async (importOriginal) => {
resolveMSTeamsMediaKind: vi.fn(({ contentType }: { contentType?: string }) =>
contentType?.startsWith("image/") ? "image" : "document",
),
isRecord: (v: unknown) => typeof v === "object" && v !== null && !Array.isArray(v),
isRecord: isMockRecord,
isUrlAllowed: vi.fn(() => true),
normalizeContentType: vi.fn((ct: string | null | undefined) => ct ?? undefined),
resolveMediaSsrfPolicy: vi.fn(() => undefined),
@@ -1,4 +1,5 @@
// Msteams helper module supports conversation store helpers behavior.
import { parseDateStringTimestampMs } from "openclaw/plugin-sdk/number-runtime";
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
import type {
MSTeamsConversationStoreEntry,
@@ -10,14 +11,7 @@ export function normalizeStoredConversationId(raw: string): string {
}
export function parseStoredConversationTimestamp(value: string | undefined): number | null {
if (!value) {
return null;
}
const parsed = Date.parse(value);
if (!Number.isFinite(parsed)) {
return null;
}
return parsed;
return parseDateStringTimestampMs(value) ?? null;
}
export function toConversationStoreEntries(
+2 -5
View File
@@ -262,11 +262,7 @@ function createPollVoteBucketStateStore(params?: MSTeamsPollStoreStateOptions) {
}
function parseTimestamp(value?: string): number | null {
if (!value) {
return null;
}
const parsed = Date.parse(value);
return Number.isFinite(parsed) ? parsed : null;
return parseDateStringTimestampMs(value) ?? null;
}
function pruneExpired<T extends { createdAt: string; updatedAt?: string }>(
@@ -477,3 +473,4 @@ export function createMSTeamsPollStoreState(
return { createPoll, getPoll, recordVote };
}
import { parseDateStringTimestampMs } from "openclaw/plugin-sdk/number-runtime";
+4 -4
View File
@@ -40,7 +40,7 @@ type OllamaDiscoveryContext = {
};
};
function readStringValue(value: unknown): string | undefined {
function readOllamaStringValue(value: unknown): string | undefined {
if (typeof value === "string") {
return normalizeOptionalString(value);
}
@@ -247,7 +247,7 @@ function hasMeaningfulExplicitOllamaConfig(
if (baseUrl) {
return resolveOllamaApiBase(baseUrl) !== OLLAMA_DEFAULT_BASE_URL;
}
if (readStringValue(providerConfig.apiKey)) {
if (readOllamaStringValue(providerConfig.apiKey)) {
return true;
}
if (providerConfig.auth) {
@@ -308,8 +308,8 @@ export async function resolveOllamaDiscoveryResult(params: {
ollamaKey.trim() !== OLLAMA_DEFAULT_API_KEY;
const explicitApiKeyRef = coerceSecretRef(explicit?.apiKey);
const explicitApiKey = explicitApiKeyRef
? readStringValue(ollamaDiscoveryKey)
: readStringValue(explicit?.apiKey);
? readOllamaStringValue(ollamaDiscoveryKey)
: readOllamaStringValue(explicit?.apiKey);
// apiKey can be an env-name or managed marker; only discoveryApiKey proves
// an explicit SecretRef resolved. Never replace its owner with local auth.
if (explicitApiKeyRef && !explicitApiKey) {
+2 -1
View File
@@ -70,7 +70,7 @@ export function redactPersistedOnePasswordResult(
if (!hasSecretValue) {
return undefined;
}
const safeDetails = isRecord(details) ? details : {};
const safeDetails = asNonArrayRecord(details);
const persisted = {
ok: true,
redacted: true,
@@ -117,3 +117,4 @@ export function createOnePasswordTool(
},
};
}
import { asNonArrayRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
+1 -4
View File
@@ -43,10 +43,6 @@ function isOpencodeGoModel(model: unknown, providerId: string): boolean {
: false;
}
function validTimeoutMs(value: unknown): number | undefined {
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : undefined;
}
function resolveTimeoutMs(model: unknown, fallbackMs: number): number {
return validTimeoutMs((model as { requestTimeoutMs?: unknown })?.requestTimeoutMs) ?? fallbackMs;
}
@@ -342,3 +338,4 @@ export function createOpencodeGoStalledStreamWrapper(
return output;
};
}
import { asPositiveFiniteNumber as validTimeoutMs } from "openclaw/plugin-sdk/number-runtime";
+4 -9
View File
@@ -5,7 +5,10 @@ import type {
SessionCatalogTranscriptItem,
SessionsCatalogReadResult,
} from "openclaw/plugin-sdk/session-catalog";
import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
import {
isRecord,
normalizeBoundedOptionalString as optionalOpenCodeString,
} from "openclaw/plugin-sdk/string-coerce-runtime";
import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
import {
materializeWindowsSpawnProgram,
@@ -93,14 +96,6 @@ type OpenCodeReadParams = {
cursor?: string;
};
function optionalOpenCodeString(value: unknown, maxLength: number): string | undefined {
if (typeof value !== "string") {
return undefined;
}
const trimmed = value.trim();
return trimmed && trimmed.length <= maxLength ? trimmed : undefined;
}
function boundedLimit(value: unknown, fallback = DEFAULT_PAGE_LIMIT): number {
if (value === undefined) {
return fallback;
+2 -5
View File
@@ -8,7 +8,7 @@ import {
normalizeOpenAICompatibleReasoningReplay,
} from "openclaw/plugin-sdk/provider-stream-shared";
import { createSubsystemLogger } from "openclaw/plugin-sdk/runtime-env";
import { readStringValue } from "openclaw/plugin-sdk/string-coerce-runtime";
import { asNonArrayRecord, readStringValue } from "openclaw/plugin-sdk/string-coerce-runtime";
import { isOpenRouterDeepSeekV4ModelId, normalizeOpenRouterModelFamilyId } from "./models.js";
import {
isOpenRouterProxyReasoningUnsupportedModel,
@@ -243,10 +243,7 @@ function applyOpenRouterDeepSeekV4ReasoningEffort(
delete payload.reasoning;
return false;
}
const reasoning =
payload.reasoning && typeof payload.reasoning === "object" && !Array.isArray(payload.reasoning)
? (payload.reasoning as Record<string, unknown>)
: {};
const reasoning = asNonArrayRecord(payload.reasoning);
reasoning.effort = effort;
payload.reasoning = reasoning;
return true;
+4 -4
View File
@@ -1,6 +1,6 @@
// Policy plugin channel, model, MCP, and network evidence.
import { normalizeProviderId } from "openclaw/plugin-sdk/provider-model-shared";
import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
import { asNonArrayRecord, isRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
import { ocPathSegment, readBooleanPath } from "./policy-state-helpers.js";
import { RESERVED_CHANNEL_CONFIG_KEYS } from "./policy-state-types.js";
import type {
@@ -128,11 +128,11 @@ export function scanPolicyNetwork(cfg: Record<string, unknown>): readonly Policy
}
export function configuredChannels(cfg: Record<string, unknown>): Record<string, unknown> {
return isRecord(cfg.channels) ? cfg.channels : {};
return asNonArrayRecord(cfg.channels);
}
function configuredMcpServers(cfg: Record<string, unknown>): Record<string, unknown> {
return isRecord(cfg.mcp) && isRecord(cfg.mcp.servers) ? cfg.mcp.servers : {};
return asNonArrayRecord(asNonArrayRecord(cfg.mcp).servers);
}
function mcpServerTransport(value: unknown): PolicyMcpServerEvidence["transport"] {
@@ -161,7 +161,7 @@ function redactMcpUrlForEvidence(raw: string): string {
}
function configuredModelProviders(cfg: Record<string, unknown>): Record<string, unknown> {
return isRecord(cfg.models) && isRecord(cfg.models.providers) ? cfg.models.providers : {};
return asNonArrayRecord(asNonArrayRecord(cfg.models).providers);
}
function networkBooleanEvidence(
+21 -17
View File
@@ -1,7 +1,11 @@
// Policy plugin data, secret, and auth evidence.
import { normalizeAgentId } from "openclaw/plugin-sdk/routing";
import { coerceSecretRef } from "openclaw/plugin-sdk/secret-input";
import { isRecord, asBoolean as readBoolean } from "openclaw/plugin-sdk/string-coerce-runtime";
import {
asBoolean as readBoolean,
asNonArrayRecord,
isRecord,
} from "openclaw/plugin-sdk/string-coerce-runtime";
import { ocPathSegment } from "./policy-state-helpers.js";
import type {
PolicyAuthProfileEvidence,
@@ -20,8 +24,8 @@ export function scanPolicySecrets(cfg: Record<string, unknown>): readonly Policy
export function scanPolicyAuthProfiles(
cfg: Record<string, unknown>,
): readonly PolicyAuthProfileEvidence[] {
const auth = isRecord(cfg.auth) ? cfg.auth : {};
const profiles = isRecord(auth.profiles) ? auth.profiles : {};
const auth = asNonArrayRecord(cfg.auth);
const profiles = asNonArrayRecord(auth.profiles);
return Object.entries(profiles)
.toSorted(([a], [b]) => a.localeCompare(b))
.map(([id, value]) => {
@@ -64,8 +68,8 @@ export function scanPolicyDataHandling(
explicit: true,
});
const diagnostics = isRecord(cfg.diagnostics) ? cfg.diagnostics : {};
const otel = isRecord(diagnostics.otel) ? diagnostics.otel : {};
const diagnostics = asNonArrayRecord(cfg.diagnostics);
const otel = asNonArrayRecord(diagnostics.otel);
const otelEnabled = diagnostics.enabled !== false && otel.enabled === true;
const tracesEnabled = otelEnabled && otel.traces !== false;
const logsEnabled = otelEnabled && otel.logs === true;
@@ -84,8 +88,8 @@ export function scanPolicyDataHandling(
explicit: otel.captureContent !== undefined,
});
const session = isRecord(cfg.session) ? cfg.session : {};
const maintenance = isRecord(session.maintenance) ? session.maintenance : {};
const session = asNonArrayRecord(cfg.session);
const maintenance = asNonArrayRecord(session.maintenance);
const retentionMode = typeof maintenance.mode === "string" ? maintenance.mode : "enforce";
entries.push({
id: "session-maintenance-mode",
@@ -130,8 +134,8 @@ function pushMemorySessionTranscriptIndexing(
entries: PolicyDataHandlingEvidence[],
cfg: Record<string, unknown>,
): void {
const memory = isRecord(cfg.memory) ? cfg.memory : {};
const defaultsMemorySearch = isRecord(memory.search) ? memory.search : {};
const memory = asNonArrayRecord(cfg.memory);
const defaultsMemorySearch = asNonArrayRecord(memory.search);
const defaultSessionMemory = memorySearchSessionTranscriptIndexing(defaultsMemorySearch);
if (defaultSessionMemory !== undefined) {
const defaultExperimental = isRecord(defaultsMemorySearch.experimental)
@@ -151,7 +155,7 @@ function pushMemorySessionTranscriptIndexing(
});
}
const agents = isRecord(cfg.agents) ? cfg.agents : {};
const agents = asNonArrayRecord(cfg.agents);
const agentEntries = isRecord(agents.entries)
? Object.entries(agents.entries).map(([entryId, value]) => ({
agentId: entryId,
@@ -193,7 +197,7 @@ function pushMemorySessionTranscriptIndexing(
return;
}
const explicit = memorySearchSessionTranscriptIndexingHasLocalConfig(memorySearch);
const experimental = isRecord(memorySearch?.experimental) ? memorySearch.experimental : {};
const experimental = asNonArrayRecord(memorySearch?.experimental);
const pathSegment = container === "list" ? `#${pathId}` : ocPathSegment(pathId);
entries.push({
id: `${agentId}-memory-session-transcripts`,
@@ -219,10 +223,10 @@ function memorySearchSessionTranscriptIndexing(
if (!isRecord(memorySearch)) {
return undefined;
}
const inherited = isRecord(inheritedMemorySearch) ? inheritedMemorySearch : {};
const inherited = asNonArrayRecord(inheritedMemorySearch);
const enabled = readBoolean(memorySearch.enabled) ?? readBoolean(inherited.enabled) ?? true;
const experimental = isRecord(memorySearch.experimental) ? memorySearch.experimental : {};
const inheritedExperimental = isRecord(inherited.experimental) ? inherited.experimental : {};
const experimental = asNonArrayRecord(memorySearch.experimental);
const inheritedExperimental = asNonArrayRecord(inherited.experimental);
const rememberAcrossConversations =
readBoolean(memorySearch.rememberAcrossConversations) ??
readBoolean(experimental.sessionMemory) ??
@@ -271,8 +275,8 @@ function memorySearchSourcesIncludeSessions(memorySearch: unknown): boolean | un
}
function scanPolicySecretProviders(cfg: Record<string, unknown>): readonly PolicySecretEvidence[] {
const secrets = isRecord(cfg.secrets) ? cfg.secrets : {};
const providers = isRecord(secrets.providers) ? secrets.providers : {};
const secrets = asNonArrayRecord(cfg.secrets);
const providers = asNonArrayRecord(secrets.providers);
return Object.entries(providers).map(([id, value]) => {
const insecure = secretProviderInsecureFlags(value);
const entry: {
@@ -298,7 +302,7 @@ function scanPolicySecretProviders(cfg: Record<string, unknown>): readonly Polic
function scanPolicySecretInputs(cfg: Record<string, unknown>): readonly PolicySecretEvidence[] {
const entries: PolicySecretEvidence[] = [];
const secrets = isRecord(cfg.secrets) ? cfg.secrets : {};
const secrets = asNonArrayRecord(cfg.secrets);
collectSecretInputs(entries, cfg, [], secretRefDefaults(secrets.defaults));
return entries;
}
@@ -1,5 +1,6 @@
// Policy plugin exec approval evidence.
import {
asNonArrayRecord,
isRecord,
asBoolean as readBoolean,
normalizeOptionalString as readString,
@@ -21,7 +22,7 @@ export function scanPolicyExecApprovals(raw: string): readonly PolicyExecApprova
return [];
}
const evidence: PolicyExecApprovalEvidence[] = [];
const defaults = isRecord(parsed.defaults) ? parsed.defaults : {};
const defaults = asNonArrayRecord(parsed.defaults);
evidence.push(
execApprovalPostureEvidence(
"defaults",
@@ -1,18 +1,18 @@
// Policy plugin gateway exposure evidence.
import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
import { asNonArrayRecord, isRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
import { ocPathSegment } from "./policy-state-helpers.js";
import type { PolicyGatewayExposureEvidence } from "./policy-state-types.js";
export function scanPolicyGatewayExposure(
cfg: Record<string, unknown>,
): readonly PolicyGatewayExposureEvidence[] {
const gateway = isRecord(cfg.gateway) ? cfg.gateway : {};
const gateway = asNonArrayRecord(cfg.gateway);
const entries: PolicyGatewayExposureEvidence[] = [];
const bind = typeof gateway.bind === "string" ? gateway.bind : undefined;
const customBindHost =
typeof gateway.customBindHost === "string" ? gateway.customBindHost : undefined;
const hasCustomBindHost = customBindHost !== undefined && customBindHost.trim() !== "";
const tailscale = isRecord(gateway.tailscale) ? gateway.tailscale : {};
const tailscale = asNonArrayRecord(gateway.tailscale);
const tailscaleForcesLoopback = tailscale.mode === "serve" || tailscale.mode === "funnel";
entries.push({
id: bind === undefined ? "gateway-bind-default" : "gateway-bind",
@@ -37,7 +37,7 @@ export function scanPolicyGatewayExposure(
});
}
const auth = isRecord(gateway.auth) ? gateway.auth : {};
const auth = asNonArrayRecord(gateway.auth);
entries.push({
id: "gateway-auth-mode",
kind: "auth",
@@ -53,7 +53,7 @@ export function scanPolicyGatewayExposure(
explicit: isRecord(auth.rateLimit),
});
const controlUi = isRecord(gateway.controlUi) ? gateway.controlUi : {};
const controlUi = asNonArrayRecord(gateway.controlUi);
pushGatewayBooleanEvidence(
entries,
"gateway-control-ui-enabled",
@@ -100,7 +100,7 @@ export function scanPolicyGatewayExposure(
});
}
const remote = isRecord(gateway.remote) ? gateway.remote : {};
const remote = asNonArrayRecord(gateway.remote);
if (gateway.mode === "remote") {
entries.push({
id: "gateway-mode-remote",
@@ -118,11 +118,11 @@ export function scanPolicyGatewayExposure(
}
}
const http = isRecord(gateway.http) ? gateway.http : {};
const endpoints = isRecord(http.endpoints) ? http.endpoints : {};
const http = asNonArrayRecord(gateway.http);
const endpoints = asNonArrayRecord(http.endpoints);
pushGatewayHttpEndpointEvidence(entries, endpoints, "chatCompletions");
pushGatewayHttpEndpointEvidence(entries, endpoints, "responses");
const nodes = isRecord(gateway.nodes) ? gateway.nodes : {};
const nodes = asNonArrayRecord(gateway.nodes);
pushGatewayNodeCommandEvidence(entries, nodes);
return entries.toSorted((a, b) => a.source.localeCompare(b.source));
}
@@ -1,5 +1,6 @@
// Policy plugin ingress evidence.
import {
asNonArrayRecord,
isRecord,
asBoolean as readBoolean,
normalizeOptionalString as readString,
@@ -25,11 +26,11 @@ const OPEN_GROUPS_DEFAULT_TO_NO_MENTION_CHANNELS = new Set(["feishu", "qa-channe
export function scanPolicyIngress(cfg: Record<string, unknown>): readonly PolicyIngressEvidence[] {
const channels = configuredChannels(cfg);
const channelDefaults = isRecord(channels.defaults) ? channels.defaults : {};
const channelDefaults = asNonArrayRecord(channels.defaults);
const inheritedChannelDefaults = pickSupportedIngressDefaults(channelDefaults);
const channelDefaultsSource = "oc://openclaw.config/channels/defaults";
const entries: PolicyIngressEvidence[] = [];
const session = isRecord(cfg.session) ? cfg.session : {};
const session = asNonArrayRecord(cfg.session);
const dmScope = readString(session.dmScope)?.toLowerCase();
entries.push({
id: "session-dm-scope",
@@ -44,7 +45,7 @@ export function scanPolicyIngress(cfg: Record<string, unknown>): readonly Policy
continue;
}
const channelSource = `oc://openclaw.config/channels/${ocPathSegment(channel)}`;
const accounts = isRecord(value.accounts) ? value.accounts : {};
const accounts = asNonArrayRecord(value.accounts);
const configuredAccounts = Object.entries(accounts).filter(
(entry): entry is [string, Record<string, unknown>] => isRecord(entry[1]),
);
@@ -413,7 +414,7 @@ function channelDmPolicy(config: Record<string, unknown>): {
readonly sourceSuffix?: string;
readonly disabledByEnabled?: boolean;
} {
const dm = isRecord(config.dm) ? config.dm : {};
const dm = asNonArrayRecord(config.dm);
if (dm.enabled === false) {
return { value: "disabled", sourceSuffix: "dm/enabled", disabledByEnabled: true };
}
+8 -10
View File
@@ -1,5 +1,6 @@
// Policy plugin sandbox posture evidence.
import {
asNonArrayRecord,
isRecord,
asBoolean as readBoolean,
normalizeOptionalString as readString,
@@ -13,9 +14,9 @@ const DEFAULT_POLICY_SANDBOX_BROWSER_NETWORK = "openclaw-sandbox-browser";
export function scanPolicySandboxPosture(
cfg: Record<string, unknown>,
): readonly PolicySandboxPostureEvidence[] {
const agents = isRecord(cfg.agents) ? cfg.agents : {};
const defaults = isRecord(agents.defaults) ? agents.defaults : {};
const defaultSandbox = isRecord(defaults.sandbox) ? defaults.sandbox : {};
const agents = asNonArrayRecord(cfg.agents);
const defaults = asNonArrayRecord(agents.defaults);
const defaultSandbox = asNonArrayRecord(defaults.sandbox);
const entries: PolicySandboxPostureEvidence[] = [];
pushSandboxPostureEvidence(entries, {
id: "agents-defaults",
@@ -33,7 +34,7 @@ export function scanPolicySandboxPosture(
}
const agentId =
typeof agent.id === "string" && agent.id.trim() !== "" ? agent.id.trim() : undefined;
const sandbox = isRecord(agent.sandbox) ? agent.sandbox : {};
const sandbox = asNonArrayRecord(agent.sandbox);
pushSandboxPostureEvidence(entries, {
id: agentId ?? `agent-${index}`,
scope: "agent",
@@ -97,8 +98,7 @@ function pushSandboxDockerPosture(
entries: PolicySandboxPostureEvidence[],
params: SandboxPostureParams,
): void {
const localDocker =
!params.sharedSandboxScope && isRecord(params.sandbox.docker) ? params.sandbox.docker : {};
const localDocker = !params.sharedSandboxScope ? asNonArrayRecord(params.sandbox.docker) : {};
const inheritedDocker = isRecord(params.inheritedSandbox.docker)
? params.inheritedSandbox.docker
: {};
@@ -183,8 +183,7 @@ function pushSandboxBrowserPosture(
entries: PolicySandboxPostureEvidence[],
params: SandboxPostureParams,
): void {
const localBrowser =
!params.sharedSandboxScope && isRecord(params.sandbox.browser) ? params.sandbox.browser : {};
const localBrowser = !params.sharedSandboxScope ? asNonArrayRecord(params.sandbox.browser) : {};
const inheritedBrowser = isRecord(params.inheritedSandbox.browser)
? params.inheritedSandbox.browser
: {};
@@ -242,8 +241,7 @@ function pushSandboxBrowserPosture(
surface: "browser",
});
} else if (params.effectiveBackend !== "docker" && params.effectiveBackend !== "podman") {
const localDocker =
!params.sharedSandboxScope && isRecord(params.sandbox.docker) ? params.sandbox.docker : {};
const localDocker = !params.sharedSandboxScope ? asNonArrayRecord(params.sandbox.docker) : {};
const inheritedDocker = isRecord(params.inheritedSandbox.docker)
? params.inheritedSandbox.docker
: {};
@@ -1,4 +1,5 @@
import {
asNonArrayRecord,
isRecord,
asBoolean as readBoolean,
normalizeOptionalString as readString,
@@ -9,10 +10,10 @@ import type { PolicyToolPostureEvidence } from "./policy-state-types.js";
export function scanPolicyToolPosture(
cfg: Record<string, unknown>,
): readonly PolicyToolPostureEvidence[] {
const globalTools = isRecord(cfg.tools) ? cfg.tools : {};
const agents = isRecord(cfg.agents) ? cfg.agents : {};
const defaults = isRecord(agents.defaults) ? agents.defaults : {};
const defaultSandbox = isRecord(defaults.sandbox) ? defaults.sandbox : {};
const globalTools = asNonArrayRecord(cfg.tools);
const agents = asNonArrayRecord(cfg.agents);
const defaults = asNonArrayRecord(agents.defaults);
const defaultSandbox = asNonArrayRecord(defaults.sandbox);
const entries: PolicyToolPostureEvidence[] = [];
pushToolPostureEvidence(entries, {
id: "tools",
@@ -36,9 +37,9 @@ export function scanPolicyToolPosture(
id: agentId ?? `agent-${index}`,
scope: "agent",
agentId,
tools: isRecord(agent.tools) ? agent.tools : {},
tools: asNonArrayRecord(agent.tools),
inheritedTools: globalTools,
sandbox: isRecord(agent.sandbox) ? agent.sandbox : {},
sandbox: asNonArrayRecord(agent.sandbox),
inheritedSandbox: defaultSandbox,
sourceBase: `oc://openclaw.config/agents/list/#${index}/tools`,
inheritedSourceBase: "oc://openclaw.config/tools",
@@ -81,8 +82,8 @@ function pushToolPostureEvidence(
}
function pushToolFsPosture(entries: PolicyToolPostureEvidence[], params: ToolPostureParams): void {
const localFs = isRecord(params.tools.fs) ? params.tools.fs : {};
const inheritedFs = isRecord(params.inheritedTools.fs) ? params.inheritedTools.fs : {};
const localFs = asNonArrayRecord(params.tools.fs);
const inheritedFs = asNonArrayRecord(params.inheritedTools.fs);
const localWorkspaceOnly = readBoolean(localFs.workspaceOnly);
const inheritedWorkspaceOnly = readBoolean(inheritedFs.workspaceOnly);
pushToolPostureValue(entries, params, {
@@ -98,8 +99,8 @@ function pushToolExecPosture(
entries: PolicyToolPostureEvidence[],
params: ToolPostureParams,
): void {
const localExec = isRecord(params.tools.exec) ? params.tools.exec : {};
const inheritedExec = isRecord(params.inheritedTools.exec) ? params.inheritedTools.exec : {};
const localExec = asNonArrayRecord(params.tools.exec);
const inheritedExec = asNonArrayRecord(params.inheritedTools.exec);
const localHost = readString(localExec.host);
const inheritedHost = readString(inheritedExec.host);
const host = localHost ?? inheritedHost ?? "auto";
@@ -142,7 +143,7 @@ function pushToolElevatedPosture(
entries: PolicyToolPostureEvidence[],
params: ToolPostureParams,
): void {
const localElevated = isRecord(params.tools.elevated) ? params.tools.elevated : {};
const localElevated = asNonArrayRecord(params.tools.elevated);
const inheritedElevated = isRecord(params.inheritedTools.elevated)
? params.inheritedTools.elevated
: {};
@@ -160,7 +161,7 @@ function pushToolElevatedPosture(
(localEnabled === undefined && inheritedEnabled !== undefined),
});
const localAllowFrom = isRecord(localElevated.allowFrom) ? localElevated.allowFrom : {};
const localAllowFrom = asNonArrayRecord(localElevated.allowFrom);
const inheritedAllowFrom = isRecord(inheritedElevated.allowFrom)
? inheritedElevated.allowFrom
: {};
@@ -1,5 +1,6 @@
// Policy plugin agent workspace evidence.
import {
asNonArrayRecord,
isRecord,
normalizeOptionalString as readString,
} from "openclaw/plugin-sdk/string-coerce-runtime";
@@ -10,10 +11,10 @@ import { toolListCoversTool } from "./tool-policy-conformance.js";
export function scanPolicyAgentWorkspace(
cfg: Record<string, unknown>,
): readonly PolicyAgentWorkspaceEvidence[] {
const agents = isRecord(cfg.agents) ? cfg.agents : {};
const defaults = isRecord(agents.defaults) ? agents.defaults : {};
const defaultSandbox = isRecord(defaults.sandbox) ? defaults.sandbox : {};
const defaultTools = isRecord(cfg.tools) ? cfg.tools : {};
const agents = asNonArrayRecord(cfg.agents);
const defaults = asNonArrayRecord(agents.defaults);
const defaultSandbox = asNonArrayRecord(defaults.sandbox);
const defaultTools = asNonArrayRecord(cfg.tools);
const entries: PolicyAgentWorkspaceEvidence[] = [];
pushAgentWorkspaceEvidence(entries, {
id: "agents-defaults",
@@ -35,8 +36,8 @@ export function scanPolicyAgentWorkspace(
}
const agentId =
typeof agent.id === "string" && agent.id.trim() !== "" ? agent.id.trim() : undefined;
const sandbox = isRecord(agent.sandbox) ? agent.sandbox : {};
const tools = isRecord(agent.tools) ? agent.tools : {};
const sandbox = asNonArrayRecord(agent.sandbox);
const tools = asNonArrayRecord(agent.tools);
pushAgentWorkspaceEvidence(entries, {
id: agentId ?? `agent-${index}`,
scope: "agent",
@@ -160,7 +161,7 @@ function agentWorkspaceToolDenyEvidence(
function configuredSandboxToolDenyEntries(
tools: Record<string, unknown>,
): readonly string[] | undefined {
const sandbox = isRecord(tools.sandbox) ? tools.sandbox : {};
const sandboxTools = isRecord(sandbox.tools) ? sandbox.tools : {};
const sandbox = asNonArrayRecord(tools.sandbox);
const sandboxTools = asNonArrayRecord(sandbox.tools);
return Array.isArray(sandboxTools.deny) ? readStringArray(sandboxTools.deny) : undefined;
}
@@ -1464,7 +1464,7 @@ export async function runMantisSlackDesktopSmoke(
timer.updatePhaseStatus("crabbox.remote_run", "accepted");
}
if (remoteRunError && !gatewaySetupCompleted && !slackQaCompleted) {
throw toErrorObject(remoteRunError);
throw toMantisError(remoteRunError);
}
if (gatewaySetup && !gatewaySetupCompleted) {
throw new Error("Slack desktop gateway setup did not report a live OpenClaw gateway.");
@@ -1580,7 +1580,7 @@ export async function runMantisSlackDesktopSmoke(
}
}
function toErrorObject(error: unknown): Error {
function toMantisError(error: unknown): Error {
return error instanceof Error ? error : new Error(formatErrorMessage(error));
}
/* oxlint-disable max-lines -- TODO: split this grandfathered oversized file. */
@@ -727,7 +727,7 @@ export async function runMantisTelegramDesktopBuilder(
timer.updatePhaseStatus("crabbox.remote_run", "accepted");
}
if (remoteRunError && !gatewaySetupCompleted) {
throw toErrorObject(remoteRunError);
throw toMantisError(remoteRunError);
}
if (gatewaySetup && !gatewaySetupCompleted) {
throw new Error("Telegram desktop builder did not report a live OpenClaw gateway.");
@@ -832,7 +832,7 @@ export async function runMantisTelegramDesktopBuilder(
}
}
function toErrorObject(error: unknown): Error {
function toMantisError(error: unknown): Error {
return error instanceof Error ? error : new Error(formatErrorMessage(error));
}
/* oxlint-disable max-lines -- TODO: split this grandfathered oversized file. */
@@ -229,11 +229,11 @@ async function runCommandWithExternalOutput(params: {
},
});
if (deferredError) {
throw toErrorObject(deferredError);
throw toMantisError(deferredError);
}
}
function toErrorObject(error: unknown): Error {
function toMantisError(error: unknown): Error {
return error instanceof Error ? error : new Error(formatErrorMessage(error));
}
+3 -11
View File
@@ -1,15 +1,7 @@
import { parseBooleanValue } from "openclaw/plugin-sdk/string-coerce-runtime";
export function parseQaProgressBooleanEnv(value: string | undefined): boolean | undefined {
const normalized = value?.trim().toLowerCase();
if (!normalized) {
return undefined;
}
if (normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on") {
return true;
}
if (normalized === "0" || normalized === "false" || normalized === "no" || normalized === "off") {
return false;
}
return undefined;
return parseBooleanValue(value);
}
export function sanitizeQaProgressValue(value: string): string {
@@ -133,7 +133,7 @@ function parsePositiveIntegerEnv(env: NodeJS.ProcessEnv, key: string, fallback:
env,
key,
fallback,
toError: (message) =>
createError: (message) =>
new QaCredentialAdminError({
code: "INVALID_ENV",
message,
@@ -145,7 +145,7 @@ function normalizeConvexSiteUrl(raw: string, env: NodeJS.ProcessEnv): string {
return normalizeQaCredentialConvexSiteUrl({
raw,
env,
toError: (message) =>
createError: (message) =>
new QaCredentialAdminError({
code: "INVALID_SITE_URL",
message,
@@ -160,7 +160,7 @@ function normalizeEndpointPrefix(value: string | undefined): string {
invalidAbsoluteMessage:
'--endpoint-prefix must be an absolute path like "/qa-credentials/v1" (not //host).',
invalidSegmentsMessage: '--endpoint-prefix must not contain backslashes or ".." path segments.',
toError: (message) =>
createError: (message) =>
new QaCredentialAdminError({
code: "INVALID_ARGUMENT",
message,
@@ -15,7 +15,7 @@ export function parseQaCredentialPositiveIntegerEnv(params: {
env: NodeJS.ProcessEnv;
fallback: number;
key: string;
toError?: ErrorFactory;
createError?: ErrorFactory;
}): number {
const raw = params.env[params.key]?.trim();
if (!raw) {
@@ -23,7 +23,7 @@ export function parseQaCredentialPositiveIntegerEnv(params: {
}
const value = parseStrictPositiveInteger(raw);
if (value === undefined) {
throw (params.toError ?? makeError)(`${params.key} must be a positive integer.`);
throw (params.createError ?? makeError)(`${params.key} must be a positive integer.`);
}
return value;
}
@@ -36,14 +36,14 @@ export function isQaCredentialTruthyOptIn(value: string | undefined) {
export function normalizeQaCredentialConvexSiteUrl(params: {
env: NodeJS.ProcessEnv;
raw: string;
toError?: ErrorFactory;
createError?: ErrorFactory;
}): string {
const toError = params.toError ?? makeError;
const createError = params.createError ?? makeError;
let url: URL;
try {
url = new URL(params.raw);
} catch {
throw toError(
throw createError(
`OPENCLAW_QA_CONVEX_SITE_URL must be a valid URL, got "${params.raw || "<empty>"}".`,
);
}
@@ -52,13 +52,13 @@ export function normalizeQaCredentialConvexSiteUrl(params: {
return text.endsWith("/") ? text.slice(0, -1) : text;
}
if (url.protocol !== "http:") {
throw toError("OPENCLAW_QA_CONVEX_SITE_URL must use https://.");
throw createError("OPENCLAW_QA_CONVEX_SITE_URL must use https://.");
}
const allowInsecureHttp = isQaCredentialTruthyOptIn(
params.env[QA_CREDENTIALS_ALLOW_INSECURE_HTTP_ENV_KEY],
);
if (!allowInsecureHttp || !isLoopbackHost(url.hostname)) {
throw toError(
throw createError(
`OPENCLAW_QA_CONVEX_SITE_URL must use https://. http:// is only allowed for loopback hosts when ${QA_CREDENTIALS_ALLOW_INSECURE_HTTP_ENV_KEY}=1.`,
);
}
@@ -70,7 +70,7 @@ export function normalizeQaCredentialEndpointPrefix(params: {
fallback?: string;
invalidAbsoluteMessage: string;
invalidSegmentsMessage: string;
toError?: ErrorFactory;
createError?: ErrorFactory;
value: string | undefined;
}): string {
const trimmed = params.value?.trim();
@@ -79,12 +79,12 @@ export function normalizeQaCredentialEndpointPrefix(params: {
}
const prefixed = trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
const normalized = prefixed.endsWith("/") ? prefixed.slice(0, -1) : prefixed;
const toError = params.toError ?? makeError;
const createError = params.createError ?? makeError;
if (!normalized.startsWith("/") || normalized.startsWith("//")) {
throw toError(params.invalidAbsoluteMessage);
throw createError(params.invalidAbsoluteMessage);
}
if (normalized.includes("\\") || normalized.split("/").some((segment) => segment === "..")) {
throw toError(params.invalidSegmentsMessage);
throw createError(params.invalidSegmentsMessage);
}
return normalized;
}
@@ -7,7 +7,7 @@
* and stay framework-agnostic.
*/
import { asOptionalObjectRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
import { readQqbotObjectRecord as asOptionalObjectRecord } from "../object-record.js";
import { DEFAULT_ACCOUNT_ID } from "./resolve.js";
// ---- Logout: clear all credential fields for an account ----
+2 -1
View File
@@ -1,6 +1,7 @@
// Qqbot plugin module implements group behavior.
import { resolveScopeRequireMention, type ScopeTree } from "openclaw/plugin-sdk/channel-policy";
import { asBoolean, asOptionalObjectRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
import { asBoolean } from "openclaw/plugin-sdk/string-coerce-runtime";
import { readQqbotObjectRecord as asOptionalObjectRecord } from "../object-record.js";
import { resolveAccountBase } from "./resolve.js";
interface GroupConfig {
@@ -9,13 +9,13 @@
*/
import {
asOptionalObjectRecord,
normalizeOptionalLowercaseString,
normalizeOptionalString,
normalizeStringifiedEntries,
readStringField,
} from "openclaw/plugin-sdk/string-coerce-runtime";
import { getPlatformAdapter } from "../adapter/index.js";
import { readQqbotObjectRecord as asOptionalObjectRecord } from "../object-record.js";
/**
* Default account ID, used for the unnamed top-level account.
@@ -0,0 +1,6 @@
import { asRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
/** Reads QQBot config objects, including array-backed legacy values. */
export function readQqbotObjectRecord(value: unknown): Record<string, unknown> | undefined {
return value !== null && typeof value === "object" ? asRecord(value) : undefined;
}
+1 -1
View File
@@ -14,11 +14,11 @@ import {
} from "openclaw/plugin-sdk/provider-http";
import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime";
import {
asOptionalObjectRecord,
normalizeOptionalString,
readStringField,
} from "openclaw/plugin-sdk/string-coerce-runtime";
import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
import { readQqbotObjectRecord as asOptionalObjectRecord } from "../object-record.js";
import { sanitizeFileName } from "./string-normalize.js";
const STT_ERROR_BODY_LIMIT_BYTES = 8 * 1024;
@@ -1,5 +1,6 @@
// Signal tests cover monitor.tool result.autostart plugin behavior.
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
import { toErrorObject as toLintErrorObject } from "openclaw/plugin-sdk/error-runtime";
import { describe, expect, it, vi } from "vitest";
import type { SignalDaemonHandle } from "./daemon.js";
import {
@@ -314,17 +315,3 @@ describe("monitorSignalProvider autostart", () => {
expect(settled).toBe(true);
});
});
function toLintErrorObject(value: unknown, fallbackMessage: string): Error {
if (value instanceof Error) {
return value;
}
if (typeof value === "string") {
return new Error(value);
}
const error = new Error(fallbackMessage, { cause: value });
if ((typeof value === "object" && value !== null) || typeof value === "function") {
Object.assign(error, value);
}
return error;
}
+2 -2
View File
@@ -4,7 +4,7 @@ import {
renderMessagePresentationTableFallbackText,
type MessagePresentationTableBlock,
} from "openclaw/plugin-sdk/interactive-runtime";
import { asOptionalRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
import { asOptionalRecord, hasNonEmptyString } from "openclaw/plugin-sdk/string-coerce-runtime";
import { escapeSlackMrkdwn } from "./monitor/mrkdwn.js";
import { renderSlackMessagePresentationTableFallbackText } from "./presentation-fallback.js";
@@ -45,7 +45,7 @@ type ParsedSlackDataTable = {
};
function readNonEmptyString(value: unknown): string | undefined {
return typeof value === "string" && value.trim().length > 0 ? value : undefined;
return hasNonEmptyString(value) ? value : undefined;
}
function countCharacters(value: string): number {

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