fix(config): accept $schema key in root config (#15280)

* fix(config): accept $schema key in root config (#14998)

* fix: strip $schema via preprocess to avoid spurious UI section

* fix(config): allow root  without zod preprocess wrapper

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
大猫子
2026-02-14 10:07:12 +08:00
committed by GitHub
parent dbe026214f
commit 13aface863
7 changed files with 36 additions and 3 deletions

View File

@@ -85,6 +85,7 @@ Docs: https://docs.openclaw.ai
- Config: keep legacy audio transcription migration strict by rejecting non-string/unsafe command tokens while still migrating valid custom script executables. (#5042) Thanks @shayan919293.
- Status/Sessions: stop clamping derived `totalTokens` to context-window size, keep prompt-token snapshots wired through session accounting, and surface context usage as unknown when fresh snapshot data is missing to avoid false 100% reports. (#15114) Thanks @echoVic.
- Providers/MiniMax: switch implicit MiniMax API-key provider from `openai-completions` to `anthropic-messages` with the correct Anthropic-compatible base URL, fixing `invalid role: developer (2013)` errors on MiniMax M2.5. (#15275) Thanks @lailoo.
- Config: accept `$schema` key in config file so JSON Schema editor tooling works without validation errors. (#14998)
- Routing: enforce strict binding-scope matching across peer/guild/team/roles so peer-scoped Discord/Slack bindings no longer match unrelated guild/team contexts or fallback tiers. (#15274) Thanks @lailoo.
- Web UI: add `img` to DOMPurify allowed tags and `src`/`alt` to allowed attributes so markdown images render in webchat instead of being stripped. (#15437) Thanks @lailoo.
- Ollama/Agents: use resolved model/provider base URLs for native `/api/chat` streaming (including aliased providers), normalize `/v1` endpoints, and forward abort + `maxTokens` stream options for reliable cancellation and token caps. (#11853) Thanks @BrokenFinger98.

View File

@@ -61,7 +61,7 @@ See the [full reference](/gateway/configuration-reference) for every available f
## Strict validation
<Warning>
OpenClaw only accepts configurations that fully match the schema. Unknown keys, malformed types, or invalid values cause the Gateway to **refuse to start**.
OpenClaw only accepts configurations that fully match the schema. Unknown keys, malformed types, or invalid values cause the Gateway to **refuse to start**. The only root-level exception is `$schema` (string), so editors can attach JSON Schema metadata.
</Warning>
When validation fails:

View File

@@ -11,7 +11,7 @@ title: "Strict Config Validation"
## Goals
- **Reject unknown config keys everywhere** (root + nested).
- **Reject unknown config keys everywhere** (root + nested), except root `$schema` metadata.
- **Reject plugin config without a schema**; dont load that plugin.
- **Remove legacy auto-migration on load**; migrations run via doctor only.
- **Auto-run doctor (dry-run) on startup**; if invalid, block non-diagnostic commands.
@@ -24,7 +24,7 @@ title: "Strict Config Validation"
## Strict validation rules
- Config must match the schema exactly at every level.
- Unknown keys are validation errors (no passthrough at root or nested).
- Unknown keys are validation errors (no passthrough at root or nested), except root `$schema` when it is a string.
- `plugins.entries.<id>.config` must be validated by the plugins schema.
- If a plugin lacks a schema, **reject plugin load** and surface a clear error.
- Unknown `channels.<id>` keys are errors unless a plugin manifest declares the channel id.

View File

@@ -0,0 +1,24 @@
import { describe, expect, it } from "vitest";
import { OpenClawSchema } from "./zod-schema.js";
describe("$schema key in config (#14998)", () => {
it("accepts config with $schema string", () => {
const result = OpenClawSchema.safeParse({
$schema: "https://openclaw.ai/config.json",
});
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.$schema).toBe("https://openclaw.ai/config.json");
}
});
it("accepts config without $schema", () => {
const result = OpenClawSchema.safeParse({});
expect(result.success).toBe(true);
});
it("rejects non-string $schema", () => {
const result = OpenClawSchema.safeParse({ $schema: 123 });
expect(result.success).toBe(false);
});
});

View File

@@ -7,6 +7,7 @@ describe("config schema", () => {
const schema = res.schema as { properties?: Record<string, unknown> };
expect(schema.properties?.gateway).toBeTruthy();
expect(schema.properties?.agents).toBeTruthy();
expect(schema.properties?.$schema).toBeUndefined();
expect(res.uiHints.gateway?.label).toBe("Gateway");
expect(res.uiHints["gateway.auth.token"]?.sensitive).toBe(true);
expect(res.version).toBeTruthy();

View File

@@ -303,6 +303,12 @@ function stripChannelSchema(schema: ConfigSchema): ConfigSchema {
if (!root || !root.properties) {
return next;
}
// Allow `$schema` in config files for editor tooling, but hide it from the
// Control UI form schema so it does not show up as a configurable section.
delete root.properties.$schema;
if (Array.isArray(root.required)) {
root.required = root.required.filter((key) => key !== "$schema");
}
const channelsNode = asSchemaObject(root.properties.channels);
if (channelsNode) {
channelsNode.properties = {};

View File

@@ -95,6 +95,7 @@ const MemorySchema = z
export const OpenClawSchema = z
.object({
$schema: z.string().optional(),
meta: z
.object({
lastTouchedVersion: z.string().optional(),