From 4ad8ed2cbef83b27505a0762ae54a6f7f3f63680 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 23 Apr 2026 05:22:01 +0100 Subject: [PATCH] refactor: type config schemas as typebox-compatible --- extensions/active-memory/config.test.ts | 3 ++- extensions/diffs/src/config.test.ts | 3 ++- extensions/memory-core/src/config.test.ts | 3 ++- extensions/memory-lancedb/config.test.ts | 3 ++- extensions/memory-wiki/src/config.test.ts | 3 ++- extensions/qqbot/src/config.test.ts | 5 +++-- src/channels/plugins/config-schema.ts | 3 ++- src/channels/plugins/types.config.ts | 4 +++- src/plugins/bundled-channel-config-metadata.ts | 3 ++- src/plugins/config-schema.ts | 3 ++- src/plugins/manifest.ts | 5 +++-- src/plugins/registry-types.ts | 3 ++- src/plugins/schema-validator.ts | 7 ++++--- src/plugins/types.ts | 3 ++- src/shared/json-schema.types.ts | 3 +++ src/wizard/setup.plugin-config.ts | 5 +++-- 16 files changed, 39 insertions(+), 20 deletions(-) create mode 100644 src/shared/json-schema.types.ts diff --git a/extensions/active-memory/config.test.ts b/extensions/active-memory/config.test.ts index e37aa8e0904..3c2a32b3ef2 100644 --- a/extensions/active-memory/config.test.ts +++ b/extensions/active-memory/config.test.ts @@ -1,10 +1,11 @@ import fs from "node:fs"; import { describe, expect, it } from "vitest"; import { validateJsonSchemaValue } from "../../src/plugins/schema-validator.js"; +import type { JsonSchemaObject } from "../../src/shared/json-schema.types.js"; const manifest = JSON.parse( fs.readFileSync(new URL("./openclaw.plugin.json", import.meta.url), "utf-8"), -) as { configSchema: Record }; +) as { configSchema: JsonSchemaObject }; describe("active-memory manifest config schema", () => { it("accepts modelFallback for CLI and config.patch flows", () => { diff --git a/extensions/diffs/src/config.test.ts b/extensions/diffs/src/config.test.ts index 4ba7d47a677..3c5b1d2e229 100644 --- a/extensions/diffs/src/config.test.ts +++ b/extensions/diffs/src/config.test.ts @@ -1,6 +1,7 @@ import fs from "node:fs"; import AjvPkg from "ajv"; import { describe, expect, it, vi } from "vitest"; +import type { JsonSchemaObject } from "../../../src/shared/json-schema.types.js"; import { DEFAULT_DIFFS_PLUGIN_SECURITY, DEFAULT_DIFFS_TOOL_DEFAULTS, @@ -39,7 +40,7 @@ const FULL_DEFAULTS = { function compileManifestConfigSchema() { const manifest = JSON.parse( fs.readFileSync(new URL("../openclaw.plugin.json", import.meta.url), "utf8"), - ) as { configSchema: Record }; + ) as { configSchema: JsonSchemaObject }; const Ajv = AjvPkg as unknown as new (opts?: object) => import("ajv").default; const ajv = new Ajv({ allErrors: true, strict: false, useDefaults: true }); return ajv.compile(manifest.configSchema); diff --git a/extensions/memory-core/src/config.test.ts b/extensions/memory-core/src/config.test.ts index c30d0616a93..fc02deb8519 100644 --- a/extensions/memory-core/src/config.test.ts +++ b/extensions/memory-core/src/config.test.ts @@ -1,10 +1,11 @@ import fs from "node:fs"; import { describe, expect, it } from "vitest"; import { validateJsonSchemaValue } from "../../../src/plugins/schema-validator.js"; +import type { JsonSchemaObject } from "../../../src/shared/json-schema.types.js"; const manifest = JSON.parse( fs.readFileSync(new URL("../openclaw.plugin.json", import.meta.url), "utf-8"), -) as { configSchema: Record }; +) as { configSchema: JsonSchemaObject }; describe("memory-core manifest config schema", () => { it("accepts dreaming phase thresholds used by QA and runtime", () => { diff --git a/extensions/memory-lancedb/config.test.ts b/extensions/memory-lancedb/config.test.ts index 4174c793cf1..5d9202cb935 100644 --- a/extensions/memory-lancedb/config.test.ts +++ b/extensions/memory-lancedb/config.test.ts @@ -1,11 +1,12 @@ import fs from "node:fs"; import { describe, expect, it } from "vitest"; import { validateJsonSchemaValue } from "../../src/plugins/schema-validator.js"; +import type { JsonSchemaObject } from "../../src/shared/json-schema.types.js"; import { memoryConfigSchema } from "./config.js"; const manifest = JSON.parse( fs.readFileSync(new URL("./openclaw.plugin.json", import.meta.url), "utf-8"), -) as { configSchema: Record }; +) as { configSchema: JsonSchemaObject }; describe("memory-lancedb config", () => { it("accepts dreaming in the manifest schema and preserves it in runtime parsing", () => { diff --git a/extensions/memory-wiki/src/config.test.ts b/extensions/memory-wiki/src/config.test.ts index bf0e1912fce..a8394342d90 100644 --- a/extensions/memory-wiki/src/config.test.ts +++ b/extensions/memory-wiki/src/config.test.ts @@ -1,6 +1,7 @@ import fs from "node:fs"; import AjvPkg from "ajv"; import { describe, expect, it } from "vitest"; +import type { JsonSchemaObject } from "../../../src/shared/json-schema.types.js"; import { DEFAULT_WIKI_RENDER_MODE, DEFAULT_WIKI_SEARCH_BACKEND, @@ -13,7 +14,7 @@ import { function compileManifestConfigSchema() { const manifest = JSON.parse( fs.readFileSync(new URL("../openclaw.plugin.json", import.meta.url), "utf8"), - ) as { configSchema: Record }; + ) as { configSchema: JsonSchemaObject }; const Ajv = AjvPkg as unknown as new (opts?: object) => import("ajv").default; const ajv = new Ajv({ allErrors: true, strict: false, useDefaults: true }); return ajv.compile(manifest.configSchema); diff --git a/extensions/qqbot/src/config.test.ts b/extensions/qqbot/src/config.test.ts index 1a895dbd1bb..7818b7ed510 100644 --- a/extensions/qqbot/src/config.test.ts +++ b/extensions/qqbot/src/config.test.ts @@ -2,6 +2,7 @@ import fs from "node:fs"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime"; import { describe, expect, it } from "vitest"; import { validateJsonSchemaValue } from "../../../src/plugins/schema-validator.js"; +import type { JsonSchemaObject } from "../../../src/shared/json-schema.types.js"; import { qqbotSetupAdapterShared } from "./bridge/config-shared.js"; import { DEFAULT_ACCOUNT_ID, @@ -16,7 +17,7 @@ describe("qqbot config", () => { it("accepts top-level speech overrides in the manifest schema", () => { const manifest = JSON.parse( fs.readFileSync(new URL("../openclaw.plugin.json", import.meta.url), "utf-8"), - ) as { configSchema: Record }; + ) as { configSchema: JsonSchemaObject }; const result = validateJsonSchemaValue({ schema: manifest.configSchema, @@ -37,7 +38,7 @@ describe("qqbot config", () => { it("accepts defaultAccount in the manifest schema", () => { const manifest = JSON.parse( fs.readFileSync(new URL("../openclaw.plugin.json", import.meta.url), "utf-8"), - ) as { configSchema: Record }; + ) as { configSchema: JsonSchemaObject }; const result = validateJsonSchemaValue({ schema: manifest.configSchema, diff --git a/src/channels/plugins/config-schema.ts b/src/channels/plugins/config-schema.ts index 8bbecf9a31e..bdeb4d3fd0c 100644 --- a/src/channels/plugins/config-schema.ts +++ b/src/channels/plugins/config-schema.ts @@ -1,5 +1,6 @@ import { z, type ZodRawShape, type ZodTypeAny } from "zod"; import { DmPolicySchema } from "../../config/zod-schema.core.js"; +import type { JsonSchemaObject } from "../../shared/json-schema.types.js"; import type { ChannelConfigRuntimeIssue, ChannelConfigRuntimeParseResult, @@ -81,7 +82,7 @@ export function buildChannelConfigSchema( schema: schemaWithJson.toJSONSchema({ target: "draft-07", unrepresentable: "any", - }) as Record, + }) as JsonSchemaObject, ...(options?.uiHints ? { uiHints: options.uiHints } : {}), runtime: { safeParse: (value) => safeParseRuntimeSchema(schema, value), diff --git a/src/channels/plugins/types.config.ts b/src/channels/plugins/types.config.ts index 6331b96846f..8bf869c5ad4 100644 --- a/src/channels/plugins/types.config.ts +++ b/src/channels/plugins/types.config.ts @@ -1,3 +1,5 @@ +import type { JsonSchemaObject } from "../../shared/json-schema.types.js"; + export type ChannelConfigUiHint = { label?: string; help?: string; @@ -29,7 +31,7 @@ export type ChannelConfigRuntimeSchema = { }; export type ChannelConfigSchema = { - schema: Record; + schema: JsonSchemaObject; uiHints?: Record; runtime?: ChannelConfigRuntimeSchema; }; diff --git a/src/plugins/bundled-channel-config-metadata.ts b/src/plugins/bundled-channel-config-metadata.ts index b8f4bdab328..a74e3ac32a6 100644 --- a/src/plugins/bundled-channel-config-metadata.ts +++ b/src/plugins/bundled-channel-config-metadata.ts @@ -2,6 +2,7 @@ import fs from "node:fs"; import path from "node:path"; import { buildChannelConfigSchema } from "../channels/plugins/config-schema.js"; import type { ChannelConfigRuntimeSchema } from "../channels/plugins/types.config.js"; +import type { JsonSchemaObject } from "../shared/json-schema.types.js"; import { normalizeBundledPluginStringList, trimBundledPluginString, @@ -26,7 +27,7 @@ const SOURCE_CONFIG_SCHEMA_CANDIDATES = [ const PUBLIC_CONFIG_SURFACE_BASENAMES = ["channel-config-api", "runtime-api", "api"] as const; type ChannelConfigSurface = { - schema: Record; + schema: JsonSchemaObject; uiHints?: Record; runtime?: ChannelConfigRuntimeSchema; }; diff --git a/src/plugins/config-schema.ts b/src/plugins/config-schema.ts index 2c74846c9f2..7bd6ca5826d 100644 --- a/src/plugins/config-schema.ts +++ b/src/plugins/config-schema.ts @@ -1,4 +1,5 @@ import { z, type ZodTypeAny } from "zod"; +import type { JsonSchemaObject } from "../shared/json-schema.types.js"; import type { PluginConfigUiHint } from "./manifest-types.js"; import type { OpenClawPluginConfigSchema } from "./types.js"; @@ -92,7 +93,7 @@ export function buildPluginConfigSchema( io: "input", unrepresentable: "any", }), - ) as Record, + ) as JsonSchemaObject, }; } diff --git a/src/plugins/manifest.ts b/src/plugins/manifest.ts index 4d477420b75..f451e6a2c02 100644 --- a/src/plugins/manifest.ts +++ b/src/plugins/manifest.ts @@ -4,6 +4,7 @@ import JSON5 from "json5"; import type { ChannelConfigRuntimeSchema } from "../channels/plugins/types.config.js"; import { MANIFEST_KEY } from "../compat/legacy-names.js"; import { matchBoundaryFileOpenFailure, openBoundaryFileSync } from "../infra/boundary-file-read.js"; +import type { JsonSchemaObject } from "../shared/json-schema.types.js"; import { normalizeOptionalString } from "../shared/string-coerce.js"; import { normalizeTrimmedStringList } from "../shared/string-normalization.js"; import { isRecord } from "../utils.js"; @@ -18,7 +19,7 @@ export const PLUGIN_MANIFEST_FILENAME = "openclaw.plugin.json"; export const PLUGIN_MANIFEST_FILENAMES = [PLUGIN_MANIFEST_FILENAME] as const; export type PluginManifestChannelConfig = { - schema: Record; + schema: JsonSchemaObject; uiHints?: Record; runtime?: ChannelConfigRuntimeSchema; label?: string; @@ -154,7 +155,7 @@ export type PluginManifestConfigContracts = { export type PluginManifest = { id: string; - configSchema: Record; + configSchema: JsonSchemaObject; enabledByDefault?: boolean; /** Legacy plugin ids that should normalize to this plugin id. */ legacyPluginIds?: string[]; diff --git a/src/plugins/registry-types.ts b/src/plugins/registry-types.ts index 0e68d1d1e58..4a77f0501d6 100644 --- a/src/plugins/registry-types.ts +++ b/src/plugins/registry-types.ts @@ -4,6 +4,7 @@ import type { ChannelPlugin } from "../channels/plugins/types.plugin.js"; import type { OperatorScope } from "../gateway/operator-scopes.js"; import type { GatewayRequestHandlers } from "../gateway/server-methods/types.js"; import type { HookEntry } from "../hooks/types.js"; +import type { JsonSchemaObject } from "../shared/json-schema.types.js"; import type { CodexAppServerExtensionFactory } from "./codex-app-server-extension-types.js"; import type { PluginActivationSource } from "./config-state.js"; import type { @@ -275,7 +276,7 @@ export type PluginRecord = { hookCount: number; configSchema: boolean; configUiHints?: Record; - configJsonSchema?: Record; + configJsonSchema?: JsonSchemaObject; contracts?: PluginManifestContracts; memorySlotSelected?: boolean; }; diff --git a/src/plugins/schema-validator.ts b/src/plugins/schema-validator.ts index e7ed9c6d26a..3246557dc53 100644 --- a/src/plugins/schema-validator.ts +++ b/src/plugins/schema-validator.ts @@ -1,6 +1,7 @@ import { createRequire } from "node:module"; import type { ErrorObject, ValidateFunction } from "ajv"; import { appendAllowedValuesHint, summarizeAllowedValues } from "../config/allowed-values.js"; +import type { JsonSchemaObject } from "../shared/json-schema.types.js"; import { sanitizeTerminalText } from "../terminal/safe-text.js"; const require = createRequire(import.meta.url); @@ -14,7 +15,7 @@ type AjvLike = { validate: (value: string) => boolean; }, ) => AjvLike; - compile: (schema: Record) => ValidateFunction; + compile: (schema: JsonSchemaObject) => ValidateFunction; }; const ajvSingletons = new Map<"default" | "defaults", AjvLike>(); @@ -48,7 +49,7 @@ function getAjv(mode: "default" | "defaults"): AjvLike { type CachedValidator = { validate: ValidateFunction; - schema: Record; + schema: JsonSchemaObject; }; const schemaCache = new Map(); @@ -158,7 +159,7 @@ function formatAjvErrors(errors: ErrorObject[] | null | undefined): JsonSchemaVa } export function validateJsonSchemaValue(params: { - schema: Record; + schema: JsonSchemaObject; cacheKey: string; value: unknown; applyDefaults?: boolean; diff --git a/src/plugins/types.ts b/src/plugins/types.ts index 30611b4bf55..97dc168ee20 100644 --- a/src/plugins/types.ts +++ b/src/plugins/types.ts @@ -48,6 +48,7 @@ import type { } from "../realtime-voice/provider-types.js"; import type { RuntimeEnv } from "../runtime.js"; import type { SecurityAuditFinding } from "../security/audit.types.js"; +import type { JsonSchemaObject } from "../shared/json-schema.types.js"; import type { SpeechDirectiveTokenParseContext, SpeechDirectiveTokenParseResult, @@ -221,7 +222,7 @@ export type OpenClawPluginConfigSchema = { parse?: (value: unknown) => unknown; validate?: (value: unknown) => PluginConfigValidation; uiHints?: Record; - jsonSchema?: Record; + jsonSchema?: JsonSchemaObject; }; export type ProviderAuthKind = "oauth" | "api_key" | "token" | "device_code" | "custom"; diff --git a/src/shared/json-schema.types.ts b/src/shared/json-schema.types.ts new file mode 100644 index 00000000000..98b295ec8c5 --- /dev/null +++ b/src/shared/json-schema.types.ts @@ -0,0 +1,3 @@ +import type { TSchema } from "typebox"; + +export type JsonSchemaObject = TSchema & Record; diff --git a/src/wizard/setup.plugin-config.ts b/src/wizard/setup.plugin-config.ts index 5180217060a..0d0ac2884f8 100644 --- a/src/wizard/setup.plugin-config.ts +++ b/src/wizard/setup.plugin-config.ts @@ -1,6 +1,7 @@ import type { OpenClawConfig } from "../config/types.openclaw.js"; import type { PluginConfigUiHint } from "../plugins/types.js"; import { getPath, setPathCreateStrict } from "../secrets/path-utils.js"; +import type { JsonSchemaObject } from "../shared/json-schema.types.js"; import type { WizardPrompter } from "./prompts.js"; /** @@ -12,7 +13,7 @@ export type ConfigurablePlugin = { /** uiHints from the plugin manifest, keyed by config field name. */ uiHints: Record; /** JSON schema from the plugin manifest (used for type/enum info). */ - jsonSchema?: Record; + jsonSchema?: JsonSchemaObject; }; type ManifestRegistryModule = typeof import("../plugins/manifest-registry.js"); @@ -31,7 +32,7 @@ type JsonSchemaProperty = { }; function resolveJsonSchemaProperty( - jsonSchema: Record | undefined, + jsonSchema: JsonSchemaObject | undefined, fieldKey: string, ): JsonSchemaProperty | undefined { if (!jsonSchema) {