refactor: type config schemas as typebox-compatible

This commit is contained in:
Peter Steinberger
2026-04-23 05:22:01 +01:00
parent cf1e48672b
commit 4ad8ed2cbe
16 changed files with 39 additions and 20 deletions

View File

@@ -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<string, unknown> };
) as { configSchema: JsonSchemaObject };
describe("active-memory manifest config schema", () => {
it("accepts modelFallback for CLI and config.patch flows", () => {

View File

@@ -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<string, unknown> };
) 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);

View File

@@ -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<string, unknown> };
) as { configSchema: JsonSchemaObject };
describe("memory-core manifest config schema", () => {
it("accepts dreaming phase thresholds used by QA and runtime", () => {

View File

@@ -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<string, unknown> };
) as { configSchema: JsonSchemaObject };
describe("memory-lancedb config", () => {
it("accepts dreaming in the manifest schema and preserves it in runtime parsing", () => {

View File

@@ -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<string, unknown> };
) 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);

View File

@@ -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<string, unknown> };
) 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<string, unknown> };
) as { configSchema: JsonSchemaObject };
const result = validateJsonSchemaValue({
schema: manifest.configSchema,

View File

@@ -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<string, unknown>,
}) as JsonSchemaObject,
...(options?.uiHints ? { uiHints: options.uiHints } : {}),
runtime: {
safeParse: (value) => safeParseRuntimeSchema(schema, value),

View File

@@ -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<string, unknown>;
schema: JsonSchemaObject;
uiHints?: Record<string, ChannelConfigUiHint>;
runtime?: ChannelConfigRuntimeSchema;
};

View File

@@ -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<string, unknown>;
schema: JsonSchemaObject;
uiHints?: Record<string, PluginConfigUiHint>;
runtime?: ChannelConfigRuntimeSchema;
};

View File

@@ -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<string, unknown>,
) as JsonSchemaObject,
};
}

View File

@@ -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<string, unknown>;
schema: JsonSchemaObject;
uiHints?: Record<string, PluginConfigUiHint>;
runtime?: ChannelConfigRuntimeSchema;
label?: string;
@@ -154,7 +155,7 @@ export type PluginManifestConfigContracts = {
export type PluginManifest = {
id: string;
configSchema: Record<string, unknown>;
configSchema: JsonSchemaObject;
enabledByDefault?: boolean;
/** Legacy plugin ids that should normalize to this plugin id. */
legacyPluginIds?: string[];

View File

@@ -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<string, PluginConfigUiHint>;
configJsonSchema?: Record<string, unknown>;
configJsonSchema?: JsonSchemaObject;
contracts?: PluginManifestContracts;
memorySlotSelected?: boolean;
};

View File

@@ -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<string, unknown>) => 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<string, unknown>;
schema: JsonSchemaObject;
};
const schemaCache = new Map<string, CachedValidator>();
@@ -158,7 +159,7 @@ function formatAjvErrors(errors: ErrorObject[] | null | undefined): JsonSchemaVa
}
export function validateJsonSchemaValue(params: {
schema: Record<string, unknown>;
schema: JsonSchemaObject;
cacheKey: string;
value: unknown;
applyDefaults?: boolean;

View File

@@ -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<string, PluginConfigUiHint>;
jsonSchema?: Record<string, unknown>;
jsonSchema?: JsonSchemaObject;
};
export type ProviderAuthKind = "oauth" | "api_key" | "token" | "device_code" | "custom";

View File

@@ -0,0 +1,3 @@
import type { TSchema } from "typebox";
export type JsonSchemaObject = TSchema & Record<string, unknown>;

View File

@@ -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<string, PluginConfigUiHint>;
/** JSON schema from the plugin manifest (used for type/enum info). */
jsonSchema?: Record<string, unknown>;
jsonSchema?: JsonSchemaObject;
};
type ManifestRegistryModule = typeof import("../plugins/manifest-registry.js");
@@ -31,7 +32,7 @@ type JsonSchemaProperty = {
};
function resolveJsonSchemaProperty(
jsonSchema: Record<string, unknown> | undefined,
jsonSchema: JsonSchemaObject | undefined,
fieldKey: string,
): JsonSchemaProperty | undefined {
if (!jsonSchema) {