refactor: centralize isPlainObject, isRecord, isErrno, isLoopbackHost utilities (#12926)

This commit is contained in:
max
2026-02-09 17:02:55 -08:00
committed by GitHub
parent 70f9edeec7
commit 8d75a496bf
37 changed files with 97 additions and 226 deletions
+1 -5
View File
@@ -10,7 +10,7 @@ import type { CliBackendConfig } from "../../config/types.js";
import type { EmbeddedContextFile } from "../pi-embedded-helpers.js"; import type { EmbeddedContextFile } from "../pi-embedded-helpers.js";
import { runExec } from "../../process/exec.js"; import { runExec } from "../../process/exec.js";
import { buildTtsSystemPromptHint } from "../../tts/tts.js"; import { buildTtsSystemPromptHint } from "../../tts/tts.js";
import { escapeRegExp } from "../../utils.js"; import { escapeRegExp, isRecord } from "../../utils.js";
import { resolveDefaultModelForAgent } from "../model-selection.js"; import { resolveDefaultModelForAgent } from "../model-selection.js";
import { detectRuntimeShell } from "../shell-utils.js"; import { detectRuntimeShell } from "../shell-utils.js";
import { buildSystemPromptParams } from "../system-prompt-params.js"; import { buildSystemPromptParams } from "../system-prompt-params.js";
@@ -280,10 +280,6 @@ function toUsage(raw: Record<string, unknown>): CliUsage | undefined {
return { input, output, cacheRead, cacheWrite, total }; return { input, output, cacheRead, cacheWrite, total };
} }
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value && typeof value === "object" && !Array.isArray(value));
}
function collectText(value: unknown): string { function collectText(value: unknown): string {
if (!value) { if (!value) {
return ""; return "";
+1 -4
View File
@@ -1,3 +1,4 @@
import { isRecord } from "../utils.js";
import { normalizeSecretInput } from "../utils/normalize-secret-input.js"; import { normalizeSecretInput } from "../utils/normalize-secret-input.js";
type MinimaxBaseResp = { type MinimaxBaseResp = {
@@ -30,10 +31,6 @@ function coerceApiHost(params: {
} }
} }
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value && typeof value === "object" && !Array.isArray(value));
}
function pickString(rec: Record<string, unknown>, key: string): string { function pickString(rec: Record<string, unknown>, key: string): string {
const v = rec[key]; const v = rec[key];
return typeof v === "string" ? v : ""; return typeof v === "string" ? v : "";
+1 -4
View File
@@ -1,6 +1,7 @@
import fs from "node:fs/promises"; import fs from "node:fs/promises";
import path from "node:path"; import path from "node:path";
import { type OpenClawConfig, loadConfig } from "../config/config.js"; import { type OpenClawConfig, loadConfig } from "../config/config.js";
import { isRecord } from "../utils.js";
import { resolveOpenClawAgentDir } from "./agent-paths.js"; import { resolveOpenClawAgentDir } from "./agent-paths.js";
import { import {
normalizeProviders, normalizeProviders,
@@ -14,10 +15,6 @@ type ModelsConfig = NonNullable<OpenClawConfig["models"]>;
const DEFAULT_MODE: NonNullable<ModelsConfig["mode"]> = "merge"; const DEFAULT_MODE: NonNullable<ModelsConfig["mode"]> = "merge";
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value && typeof value === "object" && !Array.isArray(value));
}
function mergeProviderModels(implicit: ProviderConfig, explicit: ProviderConfig): ProviderConfig { function mergeProviderModels(implicit: ProviderConfig, explicit: ProviderConfig): ProviderConfig {
const implicitModels = Array.isArray(implicit.models) ? implicit.models : []; const implicitModels = Array.isArray(implicit.models) ? implicit.models : [];
const explicitModels = Array.isArray(explicit.models) ? explicit.models : []; const explicitModels = Array.isArray(explicit.models) ? explicit.models : [];
+1 -4
View File
@@ -6,6 +6,7 @@ import type {
import type { ToolDefinition } from "@mariozechner/pi-coding-agent"; import type { ToolDefinition } from "@mariozechner/pi-coding-agent";
import type { ClientToolDefinition } from "./pi-embedded-runner/run/params.js"; import type { ClientToolDefinition } from "./pi-embedded-runner/run/params.js";
import { logDebug, logError } from "../logger.js"; import { logDebug, logError } from "../logger.js";
import { isPlainObject } from "../utils.js";
import { runBeforeToolCallHook } from "./pi-tools.before-tool-call.js"; import { runBeforeToolCallHook } from "./pi-tools.before-tool-call.js";
import { normalizeToolName } from "./tool-policy.js"; import { normalizeToolName } from "./tool-policy.js";
import { jsonResult } from "./tools/common.js"; import { jsonResult } from "./tools/common.js";
@@ -32,10 +33,6 @@ type ToolExecuteArgs = ToolDefinition["execute"] extends (...args: infer P) => u
: ToolExecuteArgsCurrent; : ToolExecuteArgsCurrent;
type ToolExecuteArgsAny = ToolExecuteArgs | ToolExecuteArgsLegacy | ToolExecuteArgsCurrent; type ToolExecuteArgsAny = ToolExecuteArgs | ToolExecuteArgsLegacy | ToolExecuteArgsCurrent;
function isPlainObject(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function isAbortSignal(value: unknown): value is AbortSignal { function isAbortSignal(value: unknown): value is AbortSignal {
return typeof value === "object" && value !== null && "aborted" in value; return typeof value === "object" && value !== null && "aborted" in value;
} }
+1 -4
View File
@@ -1,6 +1,7 @@
import type { AnyAgentTool } from "./tools/common.js"; import type { AnyAgentTool } from "./tools/common.js";
import { createSubsystemLogger } from "../logging/subsystem.js"; import { createSubsystemLogger } from "../logging/subsystem.js";
import { getGlobalHookRunner } from "../plugins/hook-runner-global.js"; import { getGlobalHookRunner } from "../plugins/hook-runner-global.js";
import { isPlainObject } from "../utils.js";
import { normalizeToolName } from "./tool-policy.js"; import { normalizeToolName } from "./tool-policy.js";
type HookContext = { type HookContext = {
@@ -12,10 +13,6 @@ type HookOutcome = { blocked: true; reason: string } | { blocked: false; params:
const log = createSubsystemLogger("agents/tools"); const log = createSubsystemLogger("agents/tools");
function isPlainObject(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
export async function runBeforeToolCallHook(args: { export async function runBeforeToolCallHook(args: {
toolName: string; toolName: string;
params: unknown; params: unknown;
+1 -5
View File
@@ -3,7 +3,7 @@ import type { CronDelivery, CronMessageChannel } from "../../cron/types.js";
import { loadConfig } from "../../config/config.js"; import { loadConfig } from "../../config/config.js";
import { normalizeCronJobCreate, normalizeCronJobPatch } from "../../cron/normalize.js"; import { normalizeCronJobCreate, normalizeCronJobPatch } from "../../cron/normalize.js";
import { parseAgentSessionKey } from "../../sessions/session-key-utils.js"; import { parseAgentSessionKey } from "../../sessions/session-key-utils.js";
import { truncateUtf16Safe } from "../../utils.js"; import { isRecord, truncateUtf16Safe } from "../../utils.js";
import { resolveSessionAgentId } from "../agent-scope.js"; import { resolveSessionAgentId } from "../agent-scope.js";
import { optionalStringEnum, stringEnum } from "../schema/typebox.js"; import { optionalStringEnum, stringEnum } from "../schema/typebox.js";
import { type AnyAgentTool, jsonResult, readStringParam } from "./common.js"; import { type AnyAgentTool, jsonResult, readStringParam } from "./common.js";
@@ -157,10 +157,6 @@ async function buildReminderContextLines(params: {
} }
} }
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function stripThreadSuffixFromSessionKey(sessionKey: string): string { function stripThreadSuffixFromSessionKey(sessionKey: string): string {
const normalized = sessionKey.toLowerCase(); const normalized = sessionKey.toLowerCase();
const idx = normalized.lastIndexOf(":thread:"); const idx = normalized.lastIndexOf(":thread:");
+3 -13
View File
@@ -1,7 +1,10 @@
import WebSocket from "ws"; import WebSocket from "ws";
import { isLoopbackHost } from "../gateway/net.js";
import { rawDataToString } from "../infra/ws.js"; import { rawDataToString } from "../infra/ws.js";
import { getChromeExtensionRelayAuthHeaders } from "./extension-relay.js"; import { getChromeExtensionRelayAuthHeaders } from "./extension-relay.js";
export { isLoopbackHost };
type CdpResponse = { type CdpResponse = {
id: number; id: number;
result?: unknown; result?: unknown;
@@ -15,19 +18,6 @@ type Pending = {
export type CdpSendFn = (method: string, params?: Record<string, unknown>) => Promise<unknown>; export type CdpSendFn = (method: string, params?: Record<string, unknown>) => Promise<unknown>;
export function isLoopbackHost(host: string) {
const h = host.trim().toLowerCase();
return (
h === "localhost" ||
h === "127.0.0.1" ||
h === "0.0.0.0" ||
h === "[::1]" ||
h === "::1" ||
h === "[::]" ||
h === "::"
);
}
export function getHeadersWithAuth(url: string, headers: Record<string, string> = {}) { export function getHeadersWithAuth(url: string, headers: Record<string, string> = {}) {
const relayHeaders = getChromeExtensionRelayAuthHeaders(url); const relayHeaders = getChromeExtensionRelayAuthHeaders(url);
const mergedHeaders = { ...relayHeaders, ...headers }; const mergedHeaders = { ...relayHeaders, ...headers };
+1 -13
View File
@@ -5,6 +5,7 @@ import {
deriveDefaultBrowserControlPort, deriveDefaultBrowserControlPort,
DEFAULT_BROWSER_CONTROL_PORT, DEFAULT_BROWSER_CONTROL_PORT,
} from "../config/port-defaults.js"; } from "../config/port-defaults.js";
import { isLoopbackHost } from "../gateway/net.js";
import { import {
DEFAULT_OPENCLAW_BROWSER_COLOR, DEFAULT_OPENCLAW_BROWSER_COLOR,
DEFAULT_OPENCLAW_BROWSER_ENABLED, DEFAULT_OPENCLAW_BROWSER_ENABLED,
@@ -42,19 +43,6 @@ export type ResolvedBrowserProfile = {
driver: "openclaw" | "extension"; driver: "openclaw" | "extension";
}; };
function isLoopbackHost(host: string) {
const h = host.trim().toLowerCase();
return (
h === "localhost" ||
h === "127.0.0.1" ||
h === "0.0.0.0" ||
h === "[::1]" ||
h === "::1" ||
h === "[::]" ||
h === "::"
);
}
function normalizeHexColor(raw: string | undefined) { function normalizeHexColor(raw: string | undefined) {
const value = (raw ?? "").trim(); const value = (raw ?? "").trim();
if (!value) { if (!value) {
+1 -13
View File
@@ -4,6 +4,7 @@ import type { Duplex } from "node:stream";
import { randomBytes } from "node:crypto"; import { randomBytes } from "node:crypto";
import { createServer } from "node:http"; import { createServer } from "node:http";
import WebSocket, { WebSocketServer } from "ws"; import WebSocket, { WebSocketServer } from "ws";
import { isLoopbackHost } from "../gateway/net.js";
import { rawDataToString } from "../infra/ws.js"; import { rawDataToString } from "../infra/ws.js";
type CdpCommand = { type CdpCommand = {
@@ -101,19 +102,6 @@ export type ChromeExtensionRelayServer = {
stop: () => Promise<void>; stop: () => Promise<void>;
}; };
function isLoopbackHost(host: string) {
const h = host.trim().toLowerCase();
return (
h === "localhost" ||
h === "127.0.0.1" ||
h === "0.0.0.0" ||
h === "[::1]" ||
h === "::1" ||
h === "[::]" ||
h === "::"
);
}
function isLoopbackAddress(ip: string | undefined): boolean { function isLoopbackAddress(ip: string | undefined): boolean {
if (!ip) { if (!ip) {
return false; return false;
+1 -5
View File
@@ -5,7 +5,7 @@ import type { PluginOrigin } from "../../plugins/types.js";
import type { ChannelMeta } from "./types.js"; import type { ChannelMeta } from "./types.js";
import { MANIFEST_KEY } from "../../compat/legacy-names.js"; import { MANIFEST_KEY } from "../../compat/legacy-names.js";
import { discoverOpenClawPlugins } from "../../plugins/discovery.js"; import { discoverOpenClawPlugins } from "../../plugins/discovery.js";
import { CONFIG_DIR, resolveUserPath } from "../../utils.js"; import { CONFIG_DIR, isRecord, resolveUserPath } from "../../utils.js";
export type ChannelUiMetaEntry = { export type ChannelUiMetaEntry = {
id: string; id: string;
@@ -61,10 +61,6 @@ const ENV_CATALOG_PATHS = ["OPENCLAW_PLUGIN_CATALOG_PATHS", "OPENCLAW_MPM_CATALO
type ManifestKey = typeof MANIFEST_KEY; type ManifestKey = typeof MANIFEST_KEY;
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value && typeof value === "object" && !Array.isArray(value));
}
function parseCatalogEntries(raw: unknown): ExternalCatalogEntry[] { function parseCatalogEntries(raw: unknown): ExternalCatalogEntry[] {
if (Array.isArray(raw)) { if (Array.isArray(raw)) {
return raw.filter((entry): entry is ExternalCatalogEntry => isRecord(entry)); return raw.filter((entry): entry is ExternalCatalogEntry => isRecord(entry));
+3 -4
View File
@@ -1,11 +1,10 @@
import { isRecord } from "../../../utils.js";
export { isRecord };
export function asString(value: unknown): string | undefined { export function asString(value: unknown): string | undefined {
return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined; return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
} }
export function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}
export function formatMatchMetadata(params: { export function formatMatchMetadata(params: {
matchKey?: unknown; matchKey?: unknown;
matchSource?: unknown; matchSource?: unknown;
+1 -5
View File
@@ -12,14 +12,10 @@ import {
} from "../config/config.js"; } from "../config/config.js";
import { applyPluginAutoEnable } from "../config/plugin-auto-enable.js"; import { applyPluginAutoEnable } from "../config/plugin-auto-enable.js";
import { note } from "../terminal/note.js"; import { note } from "../terminal/note.js";
import { resolveHomeDir } from "../utils.js"; import { isRecord, resolveHomeDir } from "../utils.js";
import { normalizeLegacyConfigValues } from "./doctor-legacy-config.js"; import { normalizeLegacyConfigValues } from "./doctor-legacy-config.js";
import { autoMigrateLegacyStateDir } from "./doctor-state-migrations.js"; import { autoMigrateLegacyStateDir } from "./doctor-state-migrations.js";
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value && typeof value === "object" && !Array.isArray(value));
}
type UnrecognizedKeysIssue = ZodIssue & { type UnrecognizedKeysIssue = ZodIssue & {
code: "unrecognized_keys"; code: "unrecognized_keys";
keys: PropertyKey[]; keys: PropertyKey[];
+2 -9
View File
@@ -1,3 +1,5 @@
import { isPlainObject } from "../utils.js";
type PathNode = Record<string, unknown>; type PathNode = Record<string, unknown>;
const BLOCKED_KEYS = new Set(["__proto__", "prototype", "constructor"]); const BLOCKED_KEYS = new Set(["__proto__", "prototype", "constructor"]);
@@ -79,12 +81,3 @@ export function getConfigValueAtPath(root: PathNode, path: string[]): unknown {
} }
return cursor; return cursor;
} }
function isPlainObject(value: unknown): value is Record<string, unknown> {
return (
typeof value === "object" &&
value !== null &&
!Array.isArray(value) &&
Object.prototype.toString.call(value) === "[object Object]"
);
}
+2 -9
View File
@@ -22,6 +22,8 @@
// Pattern for valid uppercase env var names: starts with letter or underscore, // Pattern for valid uppercase env var names: starts with letter or underscore,
// followed by letters, numbers, or underscores (all uppercase) // followed by letters, numbers, or underscores (all uppercase)
import { isPlainObject } from "../utils.js";
const ENV_VAR_NAME_PATTERN = /^[A-Z_][A-Z0-9_]*$/; const ENV_VAR_NAME_PATTERN = /^[A-Z_][A-Z0-9_]*$/;
export class MissingEnvVarError extends Error { export class MissingEnvVarError extends Error {
@@ -34,15 +36,6 @@ export class MissingEnvVarError extends Error {
} }
} }
function isPlainObject(value: unknown): value is Record<string, unknown> {
return (
typeof value === "object" &&
value !== null &&
!Array.isArray(value) &&
Object.prototype.toString.call(value) === "[object Object]"
);
}
function substituteString(value: string, env: NodeJS.ProcessEnv, configPath: string): string { function substituteString(value: string, env: NodeJS.ProcessEnv, configPath: string): string {
if (!value.includes("$")) { if (!value.includes("$")) {
return value; return value;
+1 -9
View File
@@ -13,6 +13,7 @@
import JSON5 from "json5"; import JSON5 from "json5";
import fs from "node:fs"; import fs from "node:fs";
import path from "node:path"; import path from "node:path";
import { isPlainObject } from "../utils.js";
export const INCLUDE_KEY = "$include"; export const INCLUDE_KEY = "$include";
export const MAX_INCLUDE_DEPTH = 10; export const MAX_INCLUDE_DEPTH = 10;
@@ -52,15 +53,6 @@ export class CircularIncludeError extends ConfigIncludeError {
// Utilities // Utilities
// ============================================================================ // ============================================================================
function isPlainObject(value: unknown): value is Record<string, unknown> {
return (
typeof value === "object" &&
value !== null &&
!Array.isArray(value) &&
Object.prototype.toString.call(value) === "[object Object]"
);
}
/** Deep merge: arrays concatenate, objects merge recursively, primitives: source wins */ /** Deep merge: arrays concatenate, objects merge recursively, primitives: source wins */
export function deepMerge(target: unknown, source: unknown): unknown { export function deepMerge(target: unknown, source: unknown): unknown {
if (Array.isArray(target) && Array.isArray(source)) { if (Array.isArray(target) && Array.isArray(source)) {
+2 -2
View File
@@ -10,8 +10,8 @@ export type LegacyConfigMigration = {
apply: (raw: Record<string, unknown>, changes: string[]) => void; apply: (raw: Record<string, unknown>, changes: string[]) => void;
}; };
export const isRecord = (value: unknown): value is Record<string, unknown> => import { isRecord } from "../utils.js";
Boolean(value && typeof value === "object" && !Array.isArray(value)); export { isRecord };
export const getRecord = (value: unknown): Record<string, unknown> | null => export const getRecord = (value: unknown): Record<string, unknown> | null =>
isRecord(value) ? value : null; isRecord(value) ? value : null;
+2 -4
View File
@@ -1,8 +1,6 @@
type PlainObject = Record<string, unknown>; import { isPlainObject } from "../utils.js";
function isPlainObject(value: unknown): value is PlainObject { type PlainObject = Record<string, unknown>;
return typeof value === "object" && value !== null && !Array.isArray(value);
}
export function applyMergePatch(base: unknown, patch: unknown): unknown { export function applyMergePatch(base: unknown, patch: unknown): unknown {
if (!isPlainObject(patch)) { if (!isPlainObject(patch)) {
+1 -5
View File
@@ -1,15 +1,11 @@
import type { OpenClawConfig } from "./types.js"; import type { OpenClawConfig } from "./types.js";
import { resolveUserPath } from "../utils.js"; import { isPlainObject, resolveUserPath } from "../utils.js";
const PATH_VALUE_RE = /^~(?=$|[\\/])/; const PATH_VALUE_RE = /^~(?=$|[\\/])/;
const PATH_KEY_RE = /(dir|path|paths|file|root|workspace)$/i; const PATH_KEY_RE = /(dir|path|paths|file|root|workspace)$/i;
const PATH_LIST_KEYS = new Set(["paths", "pathPrepend"]); const PATH_LIST_KEYS = new Set(["paths", "pathPrepend"]);
function isPlainObject(value: unknown): value is Record<string, unknown> {
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}
function normalizeStringValue(key: string | undefined, value: string): string { function normalizeStringValue(key: string | undefined, value: string): string {
if (!PATH_VALUE_RE.test(value.trim())) { if (!PATH_VALUE_RE.test(value.trim())) {
return value; return value;
+1 -4
View File
@@ -9,6 +9,7 @@ import {
listChatChannels, listChatChannels,
normalizeChatChannelId, normalizeChatChannelId,
} from "../channels/registry.js"; } from "../channels/registry.js";
import { isRecord } from "../utils.js";
import { hasAnyWhatsAppAuth } from "../web/accounts.js"; import { hasAnyWhatsAppAuth } from "../web/accounts.js";
type PluginEnableChange = { type PluginEnableChange = {
@@ -36,10 +37,6 @@ const PROVIDER_PLUGIN_IDS: Array<{ pluginId: string; providerId: string }> = [
{ pluginId: "minimax-portal-auth", providerId: "minimax-portal" }, { pluginId: "minimax-portal-auth", providerId: "minimax-portal" },
]; ];
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value && typeof value === "object" && !Array.isArray(value));
}
function hasNonEmptyString(value: unknown): boolean { function hasNonEmptyString(value: unknown): boolean {
return typeof value === "string" && value.trim().length > 0; return typeof value === "string" && value.trim().length > 0;
} }
+1 -9
View File
@@ -1,4 +1,5 @@
import type { OpenClawConfig } from "./types.js"; import type { OpenClawConfig } from "./types.js";
import { isPlainObject } from "../utils.js";
import { parseConfigPath, setConfigValueAtPath, unsetConfigValueAtPath } from "./config-paths.js"; import { parseConfigPath, setConfigValueAtPath, unsetConfigValueAtPath } from "./config-paths.js";
type OverrideTree = Record<string, unknown>; type OverrideTree = Record<string, unknown>;
@@ -19,15 +20,6 @@ function mergeOverrides(base: unknown, override: unknown): unknown {
return next; return next;
} }
function isPlainObject(value: unknown): value is Record<string, unknown> {
return (
typeof value === "object" &&
value !== null &&
!Array.isArray(value) &&
Object.prototype.toString.call(value) === "[object Object]"
);
}
export function getConfigOverrides(): OverrideTree { export function getConfigOverrides(): OverrideTree {
return overrides; return overrides;
} }
+1 -4
View File
@@ -9,6 +9,7 @@ import {
} from "../plugins/config-state.js"; } from "../plugins/config-state.js";
import { loadPluginManifestRegistry } from "../plugins/manifest-registry.js"; import { loadPluginManifestRegistry } from "../plugins/manifest-registry.js";
import { validateJsonSchemaValue } from "../plugins/schema-validator.js"; import { validateJsonSchemaValue } from "../plugins/schema-validator.js";
import { isRecord } from "../utils.js";
import { findDuplicateAgentDirs, formatDuplicateAgentDirError } from "./agent-dirs.js"; import { findDuplicateAgentDirs, formatDuplicateAgentDirError } from "./agent-dirs.js";
import { applyAgentDefaults, applyModelDefaults, applySessionDefaults } from "./defaults.js"; import { applyAgentDefaults, applyModelDefaults, applySessionDefaults } from "./defaults.js";
import { findLegacyConfigIssues } from "./legacy.js"; import { findLegacyConfigIssues } from "./legacy.js";
@@ -129,10 +130,6 @@ export function validateConfigObject(
}; };
} }
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value && typeof value === "object" && !Array.isArray(value));
}
export function validateConfigObjectWithPlugins(raw: unknown): export function validateConfigObjectWithPlugins(raw: unknown):
| { | {
ok: true; ok: true;
+1 -4
View File
@@ -1,5 +1,6 @@
import type { CronJobCreate, CronJobPatch } from "./types.js"; import type { CronJobCreate, CronJobPatch } from "./types.js";
import { sanitizeAgentId } from "../routing/session-key.js"; import { sanitizeAgentId } from "../routing/session-key.js";
import { isRecord } from "../utils.js";
import { parseAbsoluteTimeMs } from "./parse.js"; import { parseAbsoluteTimeMs } from "./parse.js";
import { migrateLegacyCronPayload } from "./payload-migration.js"; import { migrateLegacyCronPayload } from "./payload-migration.js";
import { inferLegacyName } from "./service/normalize.js"; import { inferLegacyName } from "./service/normalize.js";
@@ -14,10 +15,6 @@ const DEFAULT_OPTIONS: NormalizeOptions = {
applyDefaults: false, applyDefaults: false,
}; };
function isRecord(value: unknown): value is UnknownRecord {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function coerceSchedule(schedule: UnknownRecord) { function coerceSchedule(schedule: UnknownRecord) {
const next: UnknownRecord = { ...schedule }; const next: UnknownRecord = { ...schedule };
const rawKind = typeof schedule.kind === "string" ? schedule.kind.trim().toLowerCase() : ""; const rawKind = typeof schedule.kind === "string" ? schedule.kind.trim().toLowerCase() : "";
+1 -4
View File
@@ -1,5 +1,6 @@
import type { OpenClawConfig } from "../config/config.js"; import type { OpenClawConfig } from "../config/config.js";
import type { DiscordGuildChannelConfig, DiscordGuildEntry } from "../config/types.js"; import type { DiscordGuildChannelConfig, DiscordGuildEntry } from "../config/types.js";
import { isRecord } from "../utils.js";
import { resolveDiscordAccount } from "./accounts.js"; import { resolveDiscordAccount } from "./accounts.js";
import { fetchChannelPermissionsDiscord } from "./send.js"; import { fetchChannelPermissionsDiscord } from "./send.js";
@@ -22,10 +23,6 @@ export type DiscordChannelPermissionsAudit = {
const REQUIRED_CHANNEL_PERMISSIONS = ["ViewChannel", "SendMessages"] as const; const REQUIRED_CHANNEL_PERMISSIONS = ["ViewChannel", "SendMessages"] as const;
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}
function shouldAuditChannelConfig(config: DiscordGuildChannelConfig | undefined) { function shouldAuditChannelConfig(config: DiscordGuildChannelConfig | undefined) {
if (!config) { if (!config) {
return true; return true;
+1 -9
View File
@@ -2,6 +2,7 @@ import chokidar from "chokidar";
import type { OpenClawConfig, ConfigFileSnapshot, GatewayReloadMode } from "../config/config.js"; import type { OpenClawConfig, ConfigFileSnapshot, GatewayReloadMode } from "../config/config.js";
import { type ChannelId, listChannelPlugins } from "../channels/plugins/index.js"; import { type ChannelId, listChannelPlugins } from "../channels/plugins/index.js";
import { getActivePluginRegistry } from "../plugins/runtime.js"; import { getActivePluginRegistry } from "../plugins/runtime.js";
import { isPlainObject } from "../utils.js";
export type GatewayReloadSettings = { export type GatewayReloadSettings = {
mode: GatewayReloadMode; mode: GatewayReloadMode;
@@ -126,15 +127,6 @@ function matchRule(path: string): ReloadRule | null {
return null; return null;
} }
function isPlainObject(value: unknown): value is Record<string, unknown> {
return Boolean(
value &&
typeof value === "object" &&
!Array.isArray(value) &&
Object.prototype.toString.call(value) === "[object Object]",
);
}
export function diffConfigPaths(prev: unknown, next: unknown, prefix = ""): string[] { export function diffConfigPaths(prev: unknown, next: unknown, prefix = ""): string[] {
if (prev === next) { if (prev === next) {
return []; return [];
+15 -1
View File
@@ -255,6 +255,20 @@ function isValidIPv4(host: string): boolean {
}); });
} }
/**
* Check if a hostname or IP refers to the local machine.
* Handles: localhost, 127.x.x.x, ::1, [::1], ::ffff:127.x.x.x
* Note: 0.0.0.0 and :: are NOT loopback - they bind to all interfaces.
*/
export function isLoopbackHost(host: string): boolean { export function isLoopbackHost(host: string): boolean {
return isLoopbackAddress(host); if (!host) {
return false;
}
const h = host.trim().toLowerCase();
if (h === "localhost") {
return true;
}
// Handle bracketed IPv6 addresses like [::1]
const unbracket = h.startsWith("[") && h.endsWith("]") ? h.slice(1, -1) : h;
return isLoopbackAddress(unbracket);
} }
+2 -16
View File
@@ -1,3 +1,5 @@
import { isLoopbackHost } from "./net.js";
type OriginCheckResult = { ok: true } | { ok: false; reason: string }; type OriginCheckResult = { ok: true } | { ok: false; reason: string };
function normalizeHostHeader(hostHeader?: string): string { function normalizeHostHeader(hostHeader?: string): string {
@@ -38,22 +40,6 @@ function parseOrigin(
} }
} }
function isLoopbackHost(hostname: string): boolean {
if (!hostname) {
return false;
}
if (hostname === "localhost") {
return true;
}
if (hostname === "::1") {
return true;
}
if (hostname === "127.0.0.1" || hostname.startsWith("127.")) {
return true;
}
return false;
}
export function checkBrowserOrigin(params: { export function checkBrowserOrigin(params: {
requestHost?: string; requestHost?: string;
origin?: string; origin?: string;
+2 -17
View File
@@ -1,3 +1,5 @@
import { isLoopbackHost } from "../gateway/net.js";
type HostSource = string | null | undefined; type HostSource = string | null | undefined;
type CanvasHostUrlParams = { type CanvasHostUrlParams = {
@@ -9,23 +11,6 @@ type CanvasHostUrlParams = {
scheme?: "http" | "https"; scheme?: "http" | "https";
}; };
const isLoopbackHost = (value: string) => {
const normalized = value.trim().toLowerCase();
if (!normalized) {
return false;
}
if (normalized === "localhost") {
return true;
}
if (normalized === "::1") {
return true;
}
if (normalized === "0.0.0.0" || normalized === "::") {
return true;
}
return normalized.startsWith("127.");
};
const normalizeHost = (value: HostSource, rejectLoopback: boolean) => { const normalizeHost = (value: HostSource, rejectLoopback: boolean) => {
if (!value) { if (!value) {
return ""; return "";
+14
View File
@@ -12,6 +12,20 @@ export function extractErrorCode(err: unknown): string | undefined {
return undefined; return undefined;
} }
/**
* Type guard for NodeJS.ErrnoException (any error with a `code` property).
*/
export function isErrno(err: unknown): err is NodeJS.ErrnoException {
return Boolean(err && typeof err === "object" && "code" in err);
}
/**
* Check if an error has a specific errno code.
*/
export function hasErrnoCode(err: unknown, code: string): boolean {
return isErrno(err) && err.code === code;
}
export function formatErrorMessage(err: unknown): string { export function formatErrorMessage(err: unknown): string {
if (err instanceof Error) { if (err instanceof Error) {
return err.message || err.name || "Error"; return err.message || err.name || "Error";
+1 -4
View File
@@ -1,6 +1,7 @@
import net from "node:net"; import net from "node:net";
import type { PortListener, PortUsage, PortUsageStatus } from "./ports-types.js"; import type { PortListener, PortUsage, PortUsageStatus } from "./ports-types.js";
import { runCommandWithTimeout } from "../process/exec.js"; import { runCommandWithTimeout } from "../process/exec.js";
import { isErrno } from "./errors.js";
import { buildPortHints } from "./ports-format.js"; import { buildPortHints } from "./ports-format.js";
import { resolveLsofCommand } from "./ports-lsof.js"; import { resolveLsofCommand } from "./ports-lsof.js";
@@ -11,10 +12,6 @@ type CommandResult = {
error?: string; error?: string;
}; };
function isErrno(err: unknown): err is NodeJS.ErrnoException {
return Boolean(err && typeof err === "object" && "code" in err);
}
async function runCommandSafe(argv: string[], timeoutMs = 5_000): Promise<CommandResult> { async function runCommandSafe(argv: string[], timeoutMs = 5_000): Promise<CommandResult> {
try { try {
const res = await runCommandWithTimeout(argv, { timeoutMs }); const res = await runCommandWithTimeout(argv, { timeoutMs });
+1 -4
View File
@@ -4,6 +4,7 @@ import type { PortListener, PortListenerKind, PortUsage, PortUsageStatus } from
import { danger, info, shouldLogVerbose, warn } from "../globals.js"; import { danger, info, shouldLogVerbose, warn } from "../globals.js";
import { logDebug } from "../logger.js"; import { logDebug } from "../logger.js";
import { defaultRuntime } from "../runtime.js"; import { defaultRuntime } from "../runtime.js";
import { isErrno } from "./errors.js";
import { formatPortDiagnostics } from "./ports-format.js"; import { formatPortDiagnostics } from "./ports-format.js";
import { inspectPortUsage } from "./ports-inspect.js"; import { inspectPortUsage } from "./ports-inspect.js";
@@ -19,10 +20,6 @@ class PortInUseError extends Error {
} }
} }
function isErrno(err: unknown): err is NodeJS.ErrnoException {
return Boolean(err && typeof err === "object" && "code" in err);
}
export async function describePortOwner(port: number): Promise<string | undefined> { export async function describePortOwner(port: number): Promise<string | undefined> {
const diagnostics = await inspectPortUsage(port); const diagnostics = await inspectPortUsage(port);
if (diagnostics.listeners.length === 0) { if (diagnostics.listeners.length === 0) {
+1 -4
View File
@@ -1,4 +1,5 @@
import type { ProviderUsageSnapshot, UsageWindow } from "./provider-usage.types.js"; import type { ProviderUsageSnapshot, UsageWindow } from "./provider-usage.types.js";
import { isRecord } from "../utils.js";
import { fetchJson } from "./provider-usage.fetch.shared.js"; import { fetchJson } from "./provider-usage.fetch.shared.js";
import { clampPercent, PROVIDER_LABELS } from "./provider-usage.shared.js"; import { clampPercent, PROVIDER_LABELS } from "./provider-usage.shared.js";
@@ -148,10 +149,6 @@ const WINDOW_MINUTE_KEYS = [
"minutes", "minutes",
] as const; ] as const;
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value && typeof value === "object" && !Array.isArray(value));
}
function pickNumber(record: Record<string, unknown>, keys: readonly string[]): number | undefined { function pickNumber(record: Record<string, unknown>, keys: readonly string[]): number | undefined {
for (const key of keys) { for (const key of keys) {
const value = record[key]; const value = record[key];
+1 -4
View File
@@ -1,5 +1,6 @@
import { spawn } from "node:child_process"; import { spawn } from "node:child_process";
import net from "node:net"; import net from "node:net";
import { isErrno } from "./errors.js";
import { ensurePortAvailable } from "./ports.js"; import { ensurePortAvailable } from "./ports.js";
export type SshParsedTarget = { export type SshParsedTarget = {
@@ -17,10 +18,6 @@ export type SshTunnel = {
stop: () => Promise<void>; stop: () => Promise<void>;
}; };
function isErrno(err: unknown): err is NodeJS.ErrnoException {
return Boolean(err && typeof err === "object" && "code" in err);
}
export function parseSshTarget(raw: string): SshParsedTarget | null { export function parseSshTarget(raw: string): SshParsedTarget | null {
const trimmed = raw.trim().replace(/^ssh\s+/, ""); const trimmed = raw.trim().replace(/^ssh\s+/, "");
if (!trimmed) { if (!trimmed) {
+1 -4
View File
@@ -2,6 +2,7 @@ import fs from "node:fs";
import path from "node:path"; import path from "node:path";
import type { PluginConfigUiHint, PluginKind } from "./types.js"; import type { PluginConfigUiHint, PluginKind } from "./types.js";
import { MANIFEST_KEY } from "../compat/legacy-names.js"; import { MANIFEST_KEY } from "../compat/legacy-names.js";
import { isRecord } from "../utils.js";
export const PLUGIN_MANIFEST_FILENAME = "openclaw.plugin.json"; export const PLUGIN_MANIFEST_FILENAME = "openclaw.plugin.json";
export const PLUGIN_MANIFEST_FILENAMES = [PLUGIN_MANIFEST_FILENAME] as const; export const PLUGIN_MANIFEST_FILENAMES = [PLUGIN_MANIFEST_FILENAME] as const;
@@ -30,10 +31,6 @@ function normalizeStringList(value: unknown): string[] {
return value.map((entry) => (typeof entry === "string" ? entry.trim() : "")).filter(Boolean); return value.map((entry) => (typeof entry === "string" ? entry.trim() : "")).filter(Boolean);
} }
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value && typeof value === "object" && !Array.isArray(value));
}
export function resolvePluginManifestPath(rootDir: string): string { export function resolvePluginManifestPath(rootDir: string): string {
for (const filename of PLUGIN_MANIFEST_FILENAMES) { for (const filename of PLUGIN_MANIFEST_FILENAMES) {
const candidate = path.join(rootDir, filename); const candidate = path.join(rootDir, filename);
+4 -13
View File
@@ -1,5 +1,6 @@
import fs from "node:fs/promises"; import fs from "node:fs/promises";
import path from "node:path"; import path from "node:path";
import { hasErrnoCode } from "../infra/errors.js";
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Types // Types
@@ -52,16 +53,6 @@ export function isScannable(filePath: string): boolean {
return SCANNABLE_EXTENSIONS.has(path.extname(filePath).toLowerCase()); return SCANNABLE_EXTENSIONS.has(path.extname(filePath).toLowerCase());
} }
function isErrno(err: unknown, code: string): boolean {
if (!err || typeof err !== "object") {
return false;
}
if (!("code" in err)) {
return false;
}
return (err as { code?: unknown }).code === code;
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Rule definitions // Rule definitions
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -327,7 +318,7 @@ async function resolveForcedFiles(params: {
try { try {
st = await fs.stat(includePath); st = await fs.stat(includePath);
} catch (err) { } catch (err) {
if (isErrno(err, "ENOENT")) { if (hasErrnoCode(err, "ENOENT")) {
continue; continue;
} }
throw err; throw err;
@@ -374,7 +365,7 @@ async function readScannableSource(filePath: string, maxFileBytes: number): Prom
try { try {
st = await fs.stat(filePath); st = await fs.stat(filePath);
} catch (err) { } catch (err) {
if (isErrno(err, "ENOENT")) { if (hasErrnoCode(err, "ENOENT")) {
return null; return null;
} }
throw err; throw err;
@@ -385,7 +376,7 @@ async function readScannableSource(filePath: string, maxFileBytes: number): Prom
try { try {
return await fs.readFile(filePath, "utf-8"); return await fs.readFile(filePath, "utf-8");
} catch (err) { } catch (err) {
if (isErrno(err, "ENOENT")) { if (hasErrnoCode(err, "ENOENT")) {
return null; return null;
} }
throw err; throw err;
+1 -4
View File
@@ -1,4 +1,5 @@
import type { WebClient } from "@slack/web-api"; import type { WebClient } from "@slack/web-api";
import { isRecord } from "../utils.js";
import { createSlackWebClient } from "./client.js"; import { createSlackWebClient } from "./client.js";
export type SlackScopesResult = { export type SlackScopesResult = {
@@ -10,10 +11,6 @@ export type SlackScopesResult = {
type SlackScopesSource = "auth.scopes" | "apps.permissions.info"; type SlackScopesSource = "auth.scopes" | "apps.permissions.info";
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}
function collectScopes(value: unknown, into: string[]) { function collectScopes(value: unknown, into: string[]) {
if (!value) { if (!value) {
return; return;
+1 -4
View File
@@ -1,4 +1,5 @@
import type { TelegramGroupConfig } from "../config/types.js"; import type { TelegramGroupConfig } from "../config/types.js";
import { isRecord } from "../utils.js";
import { makeProxyFetch } from "./proxy.js"; import { makeProxyFetch } from "./proxy.js";
const TELEGRAM_API_BASE = "https://api.telegram.org"; const TELEGRAM_API_BASE = "https://api.telegram.org";
@@ -38,10 +39,6 @@ async function fetchWithTimeout(
} }
} }
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}
export function collectTelegramUnmentionedGroupIds( export function collectTelegramUnmentionedGroupIds(
groups: Record<string, TelegramGroupConfig> | undefined, groups: Record<string, TelegramGroupConfig> | undefined,
) { ) {
+21
View File
@@ -42,6 +42,27 @@ export function safeParseJson<T>(raw: string): T | null {
} }
} }
/**
* Type guard for plain objects (not arrays, null, Date, RegExp, etc.).
* Uses Object.prototype.toString for maximum safety.
*/
export function isPlainObject(value: unknown): value is Record<string, unknown> {
return (
typeof value === "object" &&
value !== null &&
!Array.isArray(value) &&
Object.prototype.toString.call(value) === "[object Object]"
);
}
/**
* Type guard for Record<string, unknown> (less strict than isPlainObject).
* Accepts any non-null object that isn't an array.
*/
export function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
export type WebChannel = "web"; export type WebChannel = "web";
export function assertWebChannel(input: string): asserts input is WebChannel { export function assertWebChannel(input: string): asserts input is WebChannel {