refactor: compute base config schema at runtime

This commit is contained in:
Peter Steinberger
2026-05-05 19:55:32 +01:00
parent 7dc6007aee
commit 55d1cf87d7
9 changed files with 36 additions and 29535 deletions

View File

@@ -31,7 +31,6 @@ export const RELEASE_METADATA_PATHS = new Set([
"docs/.generated/config-baseline.sha256",
"docs/install/updating.md",
"package.json",
"src/config/schema.base.generated.ts",
]);
/** @typedef {"core" | "coreTests" | "extensions" | "extensionTests" | "apps" | "docs" | "tooling" | "liveDockerTooling" | "releaseMetadata" | "all"} ChangedLane */

View File

@@ -8,7 +8,6 @@ const VERSION_ONLY_TEXT_PATHS = new Set([
"apps/ios/Config/Version.xcconfig",
"apps/ios/version.json",
"apps/macos/Sources/OpenClaw/Resources/Info.plist",
"src/config/schema.base.generated.ts",
]);
function normalizePath(input) {

View File

@@ -1,85 +1,22 @@
#!/usr/bin/env node
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { computeBaseConfigSchemaResponse } from "../src/config/schema-base.js";
import { formatGeneratedModule } from "./lib/format-generated-module.mjs";
const GENERATED_BY = "scripts/generate-base-config-schema.ts";
const DEFAULT_OUTPUT_PATH = "src/config/schema.base.generated.ts";
const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
function readIfExists(filePath: string): string | null {
try {
return fs.readFileSync(filePath, "utf8");
} catch {
return null;
}
}
function formatTypeScriptModule(source: string, outputPath: string): string {
return formatGeneratedModule(source, {
repoRoot: REPO_ROOT,
outputPath,
errorLabel: "base config schema",
export function checkBaseConfigSchema(): void {
computeBaseConfigSchemaResponse({
generatedAt: "2026-05-05T00:00:00.000Z",
});
}
export function renderBaseConfigSchemaModule(params?: { generatedAt?: string }): string {
const payload = computeBaseConfigSchemaResponse({
generatedAt: params?.generatedAt ?? new Date().toISOString(),
});
return formatTypeScriptModule(
`// Auto-generated by ${GENERATED_BY}. Do not edit directly.
import type { BaseConfigSchemaResponse } from "./schema-base.js";
export const GENERATED_BASE_CONFIG_SCHEMA: BaseConfigSchemaResponse = ${JSON.stringify(payload, null, 2)};
`,
DEFAULT_OUTPUT_PATH,
);
}
export function writeBaseConfigSchemaModule(params?: {
repoRoot?: string;
outputPath?: string;
check?: boolean;
}): { changed: boolean; wrote: boolean; outputPath: string } {
const repoRoot = path.resolve(params?.repoRoot ?? REPO_ROOT);
const outputPath = path.resolve(repoRoot, params?.outputPath ?? DEFAULT_OUTPUT_PATH);
const current = readIfExists(outputPath);
const generatedAt =
current?.match(/generatedAt:\s*"([^"]+)"/u)?.[1] ??
current?.match(/"generatedAt":\s*"([^"]+)"/u)?.[1] ??
new Date().toISOString();
const next = renderBaseConfigSchemaModule({ generatedAt });
const changed = current !== next;
if (params?.check) {
return { changed, wrote: false, outputPath };
}
if (changed) {
fs.writeFileSync(outputPath, next, "utf8");
}
return { changed, wrote: changed, outputPath };
}
const args = new Set(process.argv.slice(2));
if (args.has("--check") && args.has("--write")) {
throw new Error("Use either --check or --write, not both.");
}
if (import.meta.url === new URL(process.argv[1] ?? "", "file://").href) {
const result = writeBaseConfigSchemaModule({ check: args.has("--check") });
if (result.changed) {
if (args.has("--check")) {
console.error(
`[base-config-schema] stale generated output at ${path.relative(process.cwd(), result.outputPath)}`,
);
process.exitCode = 1;
} else {
console.log(`[base-config-schema] wrote ${path.relative(process.cwd(), result.outputPath)}`);
}
checkBaseConfigSchema();
if (args.has("--write")) {
console.log("[base-config-schema] runtime-computed; no generated file to write");
} else {
console.log("[base-config-schema] ok");
}
}

View File

@@ -1,29 +1,30 @@
import { describe, expect, it } from "vitest";
import { SENSITIVE_URL_HINT_TAG } from "../shared/net/redact-sensitive-url.js";
import { computeBaseConfigSchemaResponse } from "./schema-base.js";
import { GENERATED_BASE_CONFIG_SCHEMA } from "./schema.base.generated.js";
describe("generated base config schema", () => {
it("matches the computed base config schema payload", () => {
const BASE_CONFIG_SCHEMA = computeBaseConfigSchemaResponse({
generatedAt: "2026-05-05T00:00:00.000Z",
});
describe("base config schema", () => {
it("is deterministic for a fixed generatedAt timestamp", () => {
expect(
computeBaseConfigSchemaResponse({
generatedAt: GENERATED_BASE_CONFIG_SCHEMA.generatedAt,
generatedAt: BASE_CONFIG_SCHEMA.generatedAt,
}),
).toEqual(GENERATED_BASE_CONFIG_SCHEMA);
).toEqual(BASE_CONFIG_SCHEMA);
});
it("includes explicit URL-secret tags for sensitive URL fields", () => {
expect(GENERATED_BASE_CONFIG_SCHEMA.uiHints["mcp.servers.*.url"]?.tags).toContain(
SENSITIVE_URL_HINT_TAG,
);
expect(GENERATED_BASE_CONFIG_SCHEMA.uiHints["models.providers.*.baseUrl"]?.tags).toContain(
expect(BASE_CONFIG_SCHEMA.uiHints["mcp.servers.*.url"]?.tags).toContain(SENSITIVE_URL_HINT_TAG);
expect(BASE_CONFIG_SCHEMA.uiHints["models.providers.*.baseUrl"]?.tags).toContain(
SENSITIVE_URL_HINT_TAG,
);
});
it("omits legacy hooks.internal.handlers from the public schema payload", () => {
const hooksInternalProperties = (
GENERATED_BASE_CONFIG_SCHEMA.schema as {
BASE_CONFIG_SCHEMA.schema as {
properties?: {
hooks?: {
properties?: {
@@ -35,7 +36,7 @@ describe("generated base config schema", () => {
};
}
).properties?.hooks?.properties?.internal?.properties;
const uiHints = GENERATED_BASE_CONFIG_SCHEMA.uiHints as Record<string, unknown>;
const uiHints = BASE_CONFIG_SCHEMA.uiHints as Record<string, unknown>;
expect(hooksInternalProperties?.handlers).toBeUndefined();
expect(uiHints["hooks.internal.handlers"]).toBeUndefined();
@@ -43,7 +44,7 @@ describe("generated base config schema", () => {
it("includes videoGenerationModel in the public schema payload", () => {
const agentDefaultsProperties = (
GENERATED_BASE_CONFIG_SCHEMA.schema as {
BASE_CONFIG_SCHEMA.schema as {
properties?: {
agents?: {
properties?: {
@@ -55,7 +56,7 @@ describe("generated base config schema", () => {
};
}
).properties?.agents?.properties?.defaults?.properties;
const uiHints = GENERATED_BASE_CONFIG_SCHEMA.uiHints as Record<string, unknown>;
const uiHints = BASE_CONFIG_SCHEMA.uiHints as Record<string, unknown>;
expect(agentDefaultsProperties?.videoGenerationModel).toBeDefined();
expect(uiHints["agents.defaults.videoGenerationModel.primary"]).toBeDefined();

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@ import crypto from "node:crypto";
import { CHANNEL_IDS } from "../channels/ids.js";
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
import { GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA } from "./bundled-channel-config-metadata.generated.js";
import { GENERATED_BASE_CONFIG_SCHEMA } from "./schema.base.generated.js";
import { computeBaseConfigSchemaResponse } from "./schema-base.js";
import type { ConfigUiHint, ConfigUiHints } from "./schema.hints.js";
import { applySensitiveHints, applySensitiveUrlHints } from "./schema.hints.js";
import {
@@ -525,7 +525,7 @@ function buildBaseConfigSchema(): ConfigSchemaResponse {
if (cachedBase) {
return cachedBase;
}
const generated = GENERATED_BASE_CONFIG_SCHEMA as unknown as ConfigSchemaResponse;
const generated = computeBaseConfigSchemaResponse();
const bundledChannels = getBundledChannelSchemaMetadata();
const mergedWithoutSensitiveHints = applyHeartbeatTargetHints(
applyChannelHints(generated.uiHints, bundledChannels),

View File

@@ -3,10 +3,13 @@ import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { describe, expect, it } from "vitest";
import { GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA } from "../../config/bundled-channel-config-metadata.generated.js";
import { GENERATED_BASE_CONFIG_SCHEMA } from "../../config/schema.base.generated.js";
import { computeBaseConfigSchemaResponse } from "../../config/schema-base.js";
const SRC_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
const REPO_ROOT = resolve(SRC_ROOT, "..");
const BASE_CONFIG_SCHEMA = computeBaseConfigSchemaResponse({
generatedAt: "2026-05-05T00:00:00.000Z",
});
function readSource(path: string): string {
return readFileSync(resolve(REPO_ROOT, path), "utf8");
@@ -56,7 +59,7 @@ function asRecord(value: unknown): Record<string, unknown> {
describe("config footprint guardrails", () => {
it("keeps plugin entry config generic in the generated base schema", () => {
const root = asRecord(GENERATED_BASE_CONFIG_SCHEMA.schema);
const root = asRecord(BASE_CONFIG_SCHEMA.schema);
const plugins = asRecord(asRecord(root.properties).plugins);
const entries = asRecord(asRecord(plugins.properties).entries);
const entry = asRecord(entries.additionalProperties);
@@ -68,7 +71,7 @@ describe("config footprint guardrails", () => {
});
it("keeps retired legacy paths out of the generated base config schema", () => {
const basePaths = new Set(collectSchemaPaths(GENERATED_BASE_CONFIG_SCHEMA.schema));
const basePaths = new Set(collectSchemaPaths(BASE_CONFIG_SCHEMA.schema));
expect(
[

View File

@@ -1,7 +1,7 @@
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { GENERATED_BASE_CONFIG_SCHEMA } from "../../../src/config/schema.base.generated.js";
import { computeBaseConfigSchemaResponse } from "../../../src/config/schema-base.js";
export type ConfigHonorInventoryRow = {
key: string;
@@ -35,10 +35,13 @@ export type ConfigHonorAuditResult = {
};
const REPO_ROOT = fileURLToPath(new URL("../../../", import.meta.url));
const BASE_CONFIG_SCHEMA = computeBaseConfigSchemaResponse({
generatedAt: "2026-05-05T00:00:00.000Z",
});
function hasSchemaPath(schemaPath: string): boolean {
const segments = schemaPath.split(".");
let current: unknown = GENERATED_BASE_CONFIG_SCHEMA.schema;
let current: unknown = BASE_CONFIG_SCHEMA.schema;
for (const segment of segments) {
if (!current || typeof current !== "object") {
return false;
@@ -64,7 +67,7 @@ export function listSchemaLeafKeysForPrefixes(prefixes: string[]): string[] {
const keys = new Set<string>();
for (const prefix of prefixes) {
const segments = prefix.split(".");
let current: unknown = GENERATED_BASE_CONFIG_SCHEMA.schema;
let current: unknown = BASE_CONFIG_SCHEMA.schema;
for (const segment of segments) {
if (!current || typeof current !== "object") {
current = null;

View File

@@ -719,7 +719,6 @@ describe("scripts/changed-lanes", () => {
"apps/macos/Sources/OpenClaw/Resources/Info.plist",
"docs/.generated/config-baseline.sha256",
"package.json",
"src/config/schema.base.generated.ts",
]);
const plan = createChangedCheckPlan(result, { staged: true });