From 0c8ba6599befa028d0e3302c81a0f200f1ef0dd9 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 19 Jan 2026 03:39:36 +0000 Subject: [PATCH] fix: add plugin config schema helper --- docs/refactor/strict-config.md | 78 ++++++++++++++++++++++++++++++++++ src/plugins/config-schema.ts | 31 ++++++++++++++ 2 files changed, 109 insertions(+) create mode 100644 docs/refactor/strict-config.md create mode 100644 src/plugins/config-schema.ts diff --git a/docs/refactor/strict-config.md b/docs/refactor/strict-config.md new file mode 100644 index 000000000..03298055b --- /dev/null +++ b/docs/refactor/strict-config.md @@ -0,0 +1,78 @@ +--- +summary: "Strict config validation + doctor-only migrations" +read_when: + - Designing or implementing config validation behavior + - Working on config migrations or doctor workflows + - Handling plugin config schemas or plugin load gating +--- +# Strict config validation (doctor-only migrations) + +## Goals +- **Reject unknown config keys everywhere** (root + nested). +- **Reject plugin config without a schema**; don’t 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. + +## Non-goals +- Backward compatibility on load (legacy keys do not auto-migrate). +- Silent drops of unrecognized keys. + +## Strict validation rules +- Config must match the schema exactly at every level. +- Unknown keys are validation errors (no passthrough at root or nested). +- `plugins.entries..config` must be validated by the plugin’s schema. + - If a plugin lacks a schema, **reject plugin load** and surface a clear error. + +## Plugin schema enforcement +- Each plugin provides a strict schema for its config (no passthrough). +- Plugin load flow: + 1) Resolve plugin schema by plugin id. + 2) Validate config against the schema. + 3) If missing schema or invalid config: block plugin load, record error. +- Error message includes: + - Plugin id + - Reason (missing schema / invalid config) + - Path(s) that failed validation + +## Doctor flow +- Doctor runs **every time** config is loaded (dry-run by default). +- If config invalid: + - Print a summary + actionable errors. + - Instruct: `clawdbot doctor --fix`. +- `clawdbot doctor --fix`: + - Applies migrations. + - Removes unknown keys. + - Writes updated config. + +## Command gating (when config is invalid) +Allowed (diagnostic-only): +- `clawdbot doctor` +- `clawdbot logs` +- `clawdbot health` +- `clawdbot help` +- `clawdbot status` +- `clawdbot service` + +Everything else must hard-fail with: “Config invalid. Run `clawdbot doctor --fix`.” + +## Error UX format +- Single summary header. +- Grouped sections: + - Unknown keys (full paths) + - Legacy keys / migrations needed + - Plugin load failures (plugin id + reason + path) + +## Implementation touchpoints +- `src/config/zod-schema.ts`: remove root passthrough; strict objects everywhere. +- `src/config/zod-schema.providers.ts`: ensure strict channel schemas. +- `src/config/validation.ts`: fail on unknown keys; do not apply legacy migrations. +- `src/config/io.ts`: remove legacy auto-migrations; always run doctor dry-run. +- `src/config/legacy*.ts`: move usage to doctor only. +- `src/plugins/*`: add schema registry + gating. +- CLI command gating in `src/cli`. + +## Tests +- Unknown key rejection (root + nested). +- Plugin missing schema → plugin load blocked with clear error. +- Invalid config → gateway startup blocked except diagnostic commands. +- Doctor dry-run auto; `doctor --fix` writes corrected config. diff --git a/src/plugins/config-schema.ts b/src/plugins/config-schema.ts new file mode 100644 index 000000000..cebf0d21d --- /dev/null +++ b/src/plugins/config-schema.ts @@ -0,0 +1,31 @@ +import type { ClawdbotPluginConfigSchema } from "./types.js"; + +type Issue = { path: Array; message: string }; + +type SafeParseResult = + | { success: true; data?: unknown } + | { success: false; error: { issues: Issue[] } }; + +function error(message: string): SafeParseResult { + return { success: false, error: { issues: [{ path: [], message }] } }; +} + +export function emptyPluginConfigSchema(): ClawdbotPluginConfigSchema { + return { + safeParse(value: unknown): SafeParseResult { + if (value === undefined) return { success: true, data: undefined }; + if (!value || typeof value !== "object" || Array.isArray(value)) { + return error("expected config object"); + } + if (Object.keys(value as Record).length > 0) { + return error("config must be empty"); + } + return { success: true, data: value }; + }, + jsonSchema: { + type: "object", + additionalProperties: false, + properties: {}, + }, + }; +}