fix: enforce strict config validation
This commit is contained in:
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user