fix: enforce strict config validation

This commit is contained in:
Peter Steinberger
2026-01-19 03:38:51 +00:00
parent a9fc2ca0ef
commit d1e9490f95
53 changed files with 1025 additions and 821 deletions

View File

@@ -1,65 +1,67 @@
import {
isNixMode,
loadConfig,
migrateLegacyConfig,
readConfigFileSnapshot,
writeConfigFile,
} from "../../config/config.js";
import { danger } from "../../globals.js";
import { autoMigrateLegacyState } from "../../infra/state-migrations.js";
import { readConfigFileSnapshot } from "../../config/config.js";
import { loadAndMaybeMigrateDoctorConfig } from "../../commands/doctor-config-flow.js";
import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../../agents/agent-scope.js";
import { loadClawdbotPlugins } from "../../plugins/loader.js";
import type { RuntimeEnv } from "../../runtime.js";
const ALLOWED_INVALID_COMMANDS = new Set(["doctor", "logs", "health", "help", "status", "service"]);
function formatConfigIssues(issues: Array<{ path: string; message: string }>): string[] {
return issues.map((issue) => `- ${issue.path || "<root>"}: ${issue.message}`);
}
export async function ensureConfigReady(params: {
runtime: RuntimeEnv;
migrateState?: boolean;
commandPath?: string[];
}): Promise<void> {
await loadAndMaybeMigrateDoctorConfig({
options: { nonInteractive: true },
confirm: async () => false,
});
const snapshot = await readConfigFileSnapshot();
if (snapshot.legacyIssues.length > 0) {
if (isNixMode) {
params.runtime.error(
danger(
"Legacy config entries detected while running in Nix mode. Update your Nix config to the latest schema and retry.",
),
);
params.runtime.exit(1);
return;
}
const migrated = migrateLegacyConfig(snapshot.parsed);
if (migrated.config) {
await writeConfigFile(migrated.config);
if (migrated.changes.length > 0) {
params.runtime.log(
`Migrated legacy config entries:\n${migrated.changes
.map((entry) => `- ${entry}`)
.join("\n")}`,
);
}
} else {
const issues = snapshot.legacyIssues
.map((issue) => `- ${issue.path}: ${issue.message}`)
.join("\n");
params.runtime.error(
danger(
`Legacy config entries detected. Run "clawdbot doctor" (or ask your agent) to migrate.\n${issues}`,
),
);
params.runtime.exit(1);
return;
const command = params.commandPath?.[0];
const allowInvalid = command ? ALLOWED_INVALID_COMMANDS.has(command) : false;
const issues = snapshot.exists && !snapshot.valid ? formatConfigIssues(snapshot.issues) : [];
const legacyIssues =
snapshot.legacyIssues.length > 0
? snapshot.legacyIssues.map((issue) => `- ${issue.path}: ${issue.message}`)
: [];
const pluginIssues: string[] = [];
if (snapshot.valid) {
const workspaceDir = resolveAgentWorkspaceDir(
snapshot.config,
resolveDefaultAgentId(snapshot.config),
);
const registry = loadClawdbotPlugins({
config: snapshot.config,
workspaceDir: workspaceDir ?? undefined,
cache: false,
mode: "validate",
});
for (const diag of registry.diagnostics) {
if (diag.level !== "error") continue;
const id = diag.pluginId ? ` ${diag.pluginId}` : "";
pluginIssues.push(`- plugin${id}: ${diag.message}`);
}
}
if (snapshot.exists && !snapshot.valid) {
params.runtime.error(`Config invalid at ${snapshot.path}.`);
for (const issue of snapshot.issues) {
params.runtime.error(`- ${issue.path || "<root>"}: ${issue.message}`);
}
params.runtime.error("Run `clawdbot doctor` to repair, then retry.");
const invalid = snapshot.exists && (!snapshot.valid || pluginIssues.length > 0);
if (!invalid) return;
params.runtime.error(`Config invalid at ${snapshot.path}.`);
if (issues.length > 0) {
params.runtime.error(issues.join("\n"));
}
if (legacyIssues.length > 0) {
params.runtime.error(`Legacy config keys detected:\n${legacyIssues.join("\n")}`);
}
if (pluginIssues.length > 0) {
params.runtime.error(`Plugin config errors:\n${pluginIssues.join("\n")}`);
}
params.runtime.error("Run `clawdbot doctor --fix` to repair, then retry.");
if (!allowInvalid) {
params.runtime.exit(1);
return;
}
if (params.migrateState !== false) {
const cfg = loadConfig();
await autoMigrateLegacyState({ cfg });
}
}

View File

@@ -1,7 +1,7 @@
import type { Command } from "commander";
import { defaultRuntime } from "../../runtime.js";
import { emitCliBanner } from "../banner.js";
import { getCommandPath, hasHelpOrVersion, shouldMigrateState } from "../argv.js";
import { getCommandPath, hasHelpOrVersion } from "../argv.js";
import { ensureConfigReady } from "./config-guard.js";
function setProcessTitleForCommand(actionCommand: Command) {
@@ -20,9 +20,8 @@ export function registerPreActionHooks(program: Command, programVersion: string)
emitCliBanner(programVersion);
const argv = process.argv;
if (hasHelpOrVersion(argv)) return;
const [primary] = getCommandPath(argv, 1);
if (primary === "doctor") return;
const migrateState = shouldMigrateState(argv);
await ensureConfigReady({ runtime: defaultRuntime, migrateState });
const commandPath = getCommandPath(argv, 2);
if (commandPath[0] === "doctor") return;
await ensureConfigReady({ runtime: defaultRuntime, commandPath });
});
}

View File

@@ -20,6 +20,7 @@ export function registerMaintenanceCommands(program: Command) {
.option("--no-workspace-suggestions", "Disable workspace memory system suggestions", false)
.option("--yes", "Accept defaults without prompting", false)
.option("--repair", "Apply recommended repairs without prompting", false)
.option("--fix", "Apply recommended repairs (alias for --repair)", false)
.option("--force", "Apply aggressive repairs (overwrites custom service config)", false)
.option("--non-interactive", "Run without prompts (safe migrations only)", false)
.option("--generate-gateway-token", "Generate and configure a gateway token", false)
@@ -29,7 +30,7 @@ export function registerMaintenanceCommands(program: Command) {
await doctorCommand(defaultRuntime, {
workspaceSuggestions: opts.workspaceSuggestions,
yes: Boolean(opts.yes),
repair: Boolean(opts.repair),
repair: Boolean(opts.repair) || Boolean(opts.fix),
force: Boolean(opts.force),
nonInteractive: Boolean(opts.nonInteractive),
generateGatewayToken: Boolean(opts.generateGatewayToken),