From 5b063c2d8382620dfd6b1a74d072bf9a61297143 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 2 May 2026 16:10:38 +0100 Subject: [PATCH] fix: keep legacy config repair in doctor --- src/cli/program/config-guard.test.ts | 19 +++++ src/cli/program/config-guard.ts | 5 +- src/commands/doctor-config-preflight.test.ts | 31 ++++++++ src/commands/doctor-config-preflight.ts | 37 +++++++++- src/commands/models/load-config.runtime.ts | 2 +- src/commands/models/load-config.test.ts | 24 ++++-- src/commands/models/load-config.ts | 23 ++---- src/config/config-misc.test.ts | 66 ++++++++--------- src/config/io.ts | 78 +++++--------------- src/config/validation.ts | 15 +++- 10 files changed, 177 insertions(+), 123 deletions(-) create mode 100644 src/commands/doctor-config-preflight.test.ts diff --git a/src/cli/program/config-guard.test.ts b/src/cli/program/config-guard.test.ts index aa0b92cbf74..5d12eb201aa 100644 --- a/src/cli/program/config-guard.test.ts +++ b/src/cli/program/config-guard.test.ts @@ -3,6 +3,7 @@ import { ensureConfigReady, __test__ } from "./config-guard.js"; const loadAndMaybeMigrateDoctorConfigMock = vi.hoisted(() => vi.fn()); const readConfigFileSnapshotMock = vi.hoisted(() => vi.fn()); +const setRuntimeConfigSnapshotMock = vi.hoisted(() => vi.fn()); vi.mock("../../commands/doctor-config-preflight.js", () => ({ runDoctorConfigPreflight: loadAndMaybeMigrateDoctorConfigMock, @@ -10,6 +11,7 @@ vi.mock("../../commands/doctor-config-preflight.js", () => ({ vi.mock("../../config/config.js", () => ({ readConfigFileSnapshot: readConfigFileSnapshotMock, + setRuntimeConfigSnapshot: setRuntimeConfigSnapshotMock, })); function makeSnapshot() { @@ -105,6 +107,23 @@ describe("ensureConfigReady", () => { } }); + it("pins a valid preflight snapshot for command code reuse", async () => { + const snapshot = { + ...makeSnapshot(), + config: { runtime: true }, + runtimeConfig: { runtime: true, materialized: true }, + sourceConfig: { source: true }, + }; + readConfigFileSnapshotMock.mockResolvedValue(snapshot); + + await runEnsureConfigReady(["status"]); + + expect(setRuntimeConfigSnapshotMock).toHaveBeenCalledWith( + snapshot.runtimeConfig, + snapshot.sourceConfig, + ); + }); + it("exits for invalid config on non-allowlisted commands", async () => { setInvalidSnapshot(); const runtime = await runEnsureConfigReady(["message"]); diff --git a/src/cli/program/config-guard.ts b/src/cli/program/config-guard.ts index 87451e58cef..fff5b9051ac 100644 --- a/src/cli/program/config-guard.ts +++ b/src/cli/program/config-guard.ts @@ -1,4 +1,4 @@ -import { readConfigFileSnapshot } from "../../config/config.js"; +import { readConfigFileSnapshot, setRuntimeConfigSnapshot } from "../../config/config.js"; import type { RuntimeEnv } from "../../runtime.js"; import { shouldMigrateStateFromPath } from "../argv.js"; @@ -93,6 +93,9 @@ export async function ensureConfigReady(params: { snapshot.legacyIssues.length > 0 ? formatConfigIssueLines(snapshot.legacyIssues, "-") : []; const invalid = snapshot.exists && !snapshot.valid; + if (!invalid) { + setRuntimeConfigSnapshot(snapshot.runtimeConfig ?? snapshot.config, snapshot.sourceConfig); + } if (!invalid) { return; } diff --git a/src/commands/doctor-config-preflight.test.ts b/src/commands/doctor-config-preflight.test.ts new file mode 100644 index 00000000000..ee6bda0cc67 --- /dev/null +++ b/src/commands/doctor-config-preflight.test.ts @@ -0,0 +1,31 @@ +import { describe, expect, it } from "vitest"; +import { withTempHome, writeOpenClawConfig } from "../config/test-helpers.js"; +import { runDoctorConfigPreflight } from "./doctor-config-preflight.js"; + +describe("runDoctorConfigPreflight", () => { + it("collects legacy config issues outside the normal config read path", async () => { + await withTempHome(async (home) => { + await writeOpenClawConfig(home, { + memorySearch: { + provider: "local", + fallback: "none", + }, + }); + + const preflight = await runDoctorConfigPreflight({ + migrateState: false, + migrateLegacyConfig: false, + invalidConfigNote: false, + }); + + expect(preflight.snapshot.valid).toBe(false); + expect(preflight.snapshot.legacyIssues.some((issue) => issue.path === "memorySearch")).toBe( + true, + ); + expect((preflight.baseConfig as { memorySearch?: unknown }).memorySearch).toMatchObject({ + provider: "local", + fallback: "none", + }); + }); + }); +}); diff --git a/src/commands/doctor-config-preflight.ts b/src/commands/doctor-config-preflight.ts index f4d0dc15e67..aa74b2159b6 100644 --- a/src/commands/doctor-config-preflight.ts +++ b/src/commands/doctor-config-preflight.ts @@ -2,7 +2,13 @@ import fs from "node:fs/promises"; import path from "node:path"; import { readConfigFileSnapshot, recoverConfigFromJsonRootSuffix } from "../config/io.js"; import { formatConfigIssueLines } from "../config/issue-format.js"; +import { findLegacyConfigIssues } from "../config/legacy.js"; +import type { LegacyConfigIssue } from "../config/types.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; +import { + collectRelevantDoctorPluginIds, + listPluginDoctorLegacyConfigRules, +} from "../plugins/doctor-contract-registry.js"; import { note } from "../terminal/note.js"; import { resolveHomeDir } from "../utils.js"; import { noteIncludeConfinementWarning } from "./doctor-config-analysis.js"; @@ -55,6 +61,33 @@ export type DoctorConfigPreflightResult = { baseConfig: OpenClawConfig; }; +function collectDoctorLegacyIssues( + snapshot: Awaited>, +): LegacyConfigIssue[] { + if (!snapshot.exists) { + return []; + } + const resolvedRaw = snapshot.sourceConfig ?? snapshot.config ?? {}; + const sourceRaw = snapshot.parsed ?? resolvedRaw; + return findLegacyConfigIssues( + resolvedRaw, + sourceRaw, + listPluginDoctorLegacyConfigRules({ + pluginIds: collectRelevantDoctorPluginIds(resolvedRaw), + }), + ); +} + +function addDoctorLegacyIssues( + snapshot: Awaited>, +): Awaited> { + const legacyIssues = collectDoctorLegacyIssues(snapshot); + if (legacyIssues.length === 0) { + return snapshot; + } + return { ...snapshot, legacyIssues }; +} + export async function runDoctorConfigPreflight( options: { migrateState?: boolean; @@ -81,7 +114,7 @@ export async function runDoctorConfigPreflight( } } - let snapshot = await readConfigFileSnapshot(); + let snapshot = addDoctorLegacyIssues(await readConfigFileSnapshot()); if ( options.repairPrefixedConfig === true && snapshot.exists && @@ -89,7 +122,7 @@ export async function runDoctorConfigPreflight( (await recoverConfigFromJsonRootSuffix(snapshot)) ) { note("Removed non-JSON prefix from openclaw.json; original saved as .clobbered.*.", "Config"); - snapshot = await readConfigFileSnapshot(); + snapshot = addDoctorLegacyIssues(await readConfigFileSnapshot()); } const invalidConfigNote = options.invalidConfigNote ?? "Config invalid; doctor will run with best-effort config."; diff --git a/src/commands/models/load-config.runtime.ts b/src/commands/models/load-config.runtime.ts index 978b24cf682..e9edcb5ad07 100644 --- a/src/commands/models/load-config.runtime.ts +++ b/src/commands/models/load-config.runtime.ts @@ -1,7 +1,7 @@ export { getModelsCommandSecretTargetIds } from "../../cli/command-secret-targets.js"; export { getRuntimeConfig, - readSourceConfigSnapshotForWrite, + getRuntimeConfigSourceSnapshot, setRuntimeConfigSnapshot, type OpenClawConfig, } from "../../config/config.js"; diff --git a/src/commands/models/load-config.test.ts b/src/commands/models/load-config.test.ts index b5b03edcd08..0535adaa776 100644 --- a/src/commands/models/load-config.test.ts +++ b/src/commands/models/load-config.test.ts @@ -2,7 +2,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; const mocks = vi.hoisted(() => ({ getRuntimeConfig: vi.fn(), - readSourceConfigSnapshotForWrite: vi.fn(), + getRuntimeConfigSourceSnapshot: vi.fn(), setRuntimeConfigSnapshot: vi.fn(), resolveCommandSecretRefsViaGateway: vi.fn(), getModelsCommandSecretTargetIds: vi.fn(), @@ -10,7 +10,7 @@ const mocks = vi.hoisted(() => ({ vi.mock("../../config/config.js", () => ({ getRuntimeConfig: mocks.getRuntimeConfig, - readSourceConfigSnapshotForWrite: mocks.readSourceConfigSnapshotForWrite, + getRuntimeConfigSourceSnapshot: mocks.getRuntimeConfigSourceSnapshot, setRuntimeConfigSnapshot: mocks.setRuntimeConfigSnapshot, })); @@ -35,10 +35,7 @@ describe("models load-config", () => { function mockResolvedConfigFlow(params: { sourceConfig: unknown; diagnostics: string[] }) { mocks.getRuntimeConfig.mockReturnValue(runtimeConfig); - mocks.readSourceConfigSnapshotForWrite.mockResolvedValue({ - snapshot: { valid: true, sourceConfig: params.sourceConfig, resolved: params.sourceConfig }, - writeOptions: {}, - }); + mocks.getRuntimeConfigSourceSnapshot.mockReturnValue(params.sourceConfig); mocks.getModelsCommandSecretTargetIds.mockReturnValue(targetIds); mocks.resolveCommandSecretRefsViaGateway.mockResolvedValue({ resolvedConfig, @@ -88,4 +85,19 @@ describe("models load-config", () => { await expect(loadModelsConfig({ commandName: "models list" })).resolves.toBe(resolvedConfig); expect(mocks.setRuntimeConfigSnapshot).toHaveBeenCalledWith(resolvedConfig, sourceConfig); }); + + it("does not reread config when no source snapshot is pinned", async () => { + mocks.getRuntimeConfig.mockReturnValue(runtimeConfig); + mocks.getRuntimeConfigSourceSnapshot.mockReturnValue(null); + mocks.getModelsCommandSecretTargetIds.mockReturnValue(targetIds); + mocks.resolveCommandSecretRefsViaGateway.mockResolvedValue({ + resolvedConfig, + diagnostics: [], + }); + + const result = await loadModelsConfigWithSource({ commandName: "models list" }); + + expect(result.sourceConfig).toBe(runtimeConfig); + expect(mocks.setRuntimeConfigSnapshot).toHaveBeenCalledWith(resolvedConfig); + }); }); diff --git a/src/commands/models/load-config.ts b/src/commands/models/load-config.ts index b60cc1687a3..66bee90e157 100644 --- a/src/commands/models/load-config.ts +++ b/src/commands/models/load-config.ts @@ -2,7 +2,7 @@ import { resolveCommandConfigWithSecrets } from "../../cli/command-config-resolu import type { RuntimeEnv } from "../../runtime.js"; import { getRuntimeConfig, - readSourceConfigSnapshotForWrite, + getRuntimeConfigSourceSnapshot, setRuntimeConfigSnapshot, type OpenClawConfig, getModelsCommandSecretTargetIds, @@ -14,31 +14,24 @@ export type LoadedModelsConfig = { diagnostics: string[]; }; -async function loadSourceConfigSnapshot(fallback: OpenClawConfig): Promise { - try { - const { snapshot } = await readSourceConfigSnapshotForWrite(); - if (snapshot.valid) { - return snapshot.sourceConfig; - } - } catch { - // Fall back to runtime-loaded config if source snapshot cannot be read. - } - return fallback; -} - export async function loadModelsConfigWithSource(params: { commandName: string; runtime?: RuntimeEnv; }): Promise { const runtimeConfig = getRuntimeConfig(); - const sourceConfig = await loadSourceConfigSnapshot(runtimeConfig); + const pinnedSourceConfig = getRuntimeConfigSourceSnapshot(); + const sourceConfig = pinnedSourceConfig ?? runtimeConfig; const { resolvedConfig, diagnostics } = await resolveCommandConfigWithSecrets({ config: runtimeConfig, commandName: params.commandName, targetIds: getModelsCommandSecretTargetIds(), runtime: params.runtime, }); - setRuntimeConfigSnapshot(resolvedConfig, sourceConfig); + if (pinnedSourceConfig) { + setRuntimeConfigSnapshot(resolvedConfig, sourceConfig); + } else { + setRuntimeConfigSnapshot(resolvedConfig); + } return { sourceConfig, resolvedConfig, diff --git a/src/config/config-misc.test.ts b/src/config/config-misc.test.ts index cbaf13d53c9..e64e44b8e28 100644 --- a/src/config/config-misc.test.ts +++ b/src/config/config-misc.test.ts @@ -824,7 +824,7 @@ describe("config strict validation", () => { } }); - it("accepts top-level memorySearch via auto-migration and reports legacyIssues", async () => { + it("rejects top-level memorySearch without read-time auto-migration", async () => { await withTempHome(async (home) => { await writeOpenClawConfig(home, { memorySearch: { @@ -836,19 +836,19 @@ describe("config strict validation", () => { const snap = await readConfigFileSnapshot(); - expect(snap.issues).toEqual([]); - expect(snap.valid).toBe(true); - expect(snap.legacyIssues.some((issue) => issue.path === "memorySearch")).toBe(true); - expect(snap.sourceConfig.agents?.defaults?.memorySearch).toMatchObject({ + expect(snap.valid).toBe(false); + expect(snap.issues.some((issue) => issue.path === "memorySearch")).toBe(true); + expect(snap.legacyIssues).toEqual([]); + expect((snap.sourceConfig as { memorySearch?: unknown }).memorySearch).toMatchObject({ provider: "local", fallback: "none", query: { maxResults: 7 }, }); - expect((snap.sourceConfig as { memorySearch?: unknown }).memorySearch).toBeUndefined(); + expect(snap.sourceConfig.agents?.defaults?.memorySearch).toBeUndefined(); }); }); - it("accepts top-level heartbeat agent settings via auto-migration and reports legacyIssues", async () => { + it("rejects top-level heartbeat agent settings without read-time auto-migration", async () => { await withTempHome(async (home) => { await writeOpenClawConfig(home, { heartbeat: { @@ -859,17 +859,18 @@ describe("config strict validation", () => { const snap = await readConfigFileSnapshot(); - expect(snap.valid).toBe(true); - expect(snap.legacyIssues.some((issue) => issue.path === "heartbeat")).toBe(true); - expect(snap.sourceConfig.agents?.defaults?.heartbeat).toMatchObject({ + expect(snap.valid).toBe(false); + expect(snap.issues.some((issue) => issue.path === "heartbeat")).toBe(true); + expect(snap.legacyIssues).toEqual([]); + expect((snap.sourceConfig as { heartbeat?: unknown }).heartbeat).toMatchObject({ every: "30m", model: "anthropic/claude-3-5-haiku-20241022", }); - expect((snap.sourceConfig as { heartbeat?: unknown }).heartbeat).toBeUndefined(); + expect(snap.sourceConfig.agents?.defaults?.heartbeat).toBeUndefined(); }); }); - it("accepts top-level heartbeat visibility via auto-migration and reports legacyIssues", async () => { + it("rejects top-level heartbeat visibility without read-time auto-migration", async () => { await withTempHome(async (home) => { await writeOpenClawConfig(home, { heartbeat: { @@ -881,14 +882,15 @@ describe("config strict validation", () => { const snap = await readConfigFileSnapshot(); - expect(snap.valid).toBe(true); - expect(snap.legacyIssues.some((issue) => issue.path === "heartbeat")).toBe(true); - expect(snap.sourceConfig.channels?.defaults?.heartbeat).toMatchObject({ + expect(snap.valid).toBe(false); + expect(snap.issues.some((issue) => issue.path === "heartbeat")).toBe(true); + expect(snap.legacyIssues).toEqual([]); + expect((snap.sourceConfig as { heartbeat?: unknown }).heartbeat).toMatchObject({ showOk: true, showAlerts: false, useIndicator: true, }); - expect((snap.sourceConfig as { heartbeat?: unknown }).heartbeat).toBeUndefined(); + expect(snap.sourceConfig.channels?.defaults?.heartbeat).toBeUndefined(); }); }); @@ -930,7 +932,7 @@ describe("config strict validation", () => { expect(next?.messages?.tts?.elevenlabs).toBeUndefined(); }); - it("accepts legacy sandbox perSession via auto-migration and reports legacyIssues", async () => { + it("rejects legacy sandbox perSession without read-time auto-migration", async () => { await withTempHome(async (home) => { await writeOpenClawConfig(home, { agents: { @@ -952,21 +954,16 @@ describe("config strict validation", () => { const snap = await readConfigFileSnapshot(); - expect(snap.valid).toBe(true); - expect(snap.legacyIssues.some((issue) => issue.path === "agents.defaults.sandbox")).toBe( - true, - ); - expect(snap.legacyIssues.some((issue) => issue.path === "agents.list")).toBe(true); - expect(snap.sourceConfig.agents?.defaults?.sandbox).toEqual({ - scope: "session", - }); - expect(snap.sourceConfig.agents?.list?.[0]?.sandbox).toEqual({ - scope: "shared", - }); + expect(snap.valid).toBe(false); + expect(snap.issues.some((issue) => issue.path === "agents.defaults.sandbox")).toBe(true); + expect(snap.issues.some((issue) => issue.path === "agents.list")).toBe(true); + expect(snap.legacyIssues).toEqual([]); + expect(snap.sourceConfig.agents?.defaults?.sandbox).toEqual({ perSession: true }); + expect(snap.sourceConfig.agents?.list?.[0]?.sandbox).toEqual({ perSession: false }); }); }); - it("does not treat resolved-only gateway.bind aliases as source-literal legacy or invalid", async () => { + it("rejects resolved-only gateway.bind aliases as invalid schema values, not legacy", async () => { await withTempHome(async (home) => { await writeOpenClawConfig(home, { gateway: { bind: "${OPENCLAW_BIND}" }, @@ -976,9 +973,9 @@ describe("config strict validation", () => { process.env.OPENCLAW_BIND = "0.0.0.0"; try { const snap = await readConfigFileSnapshot(); - expect(snap.valid).toBe(true); + expect(snap.valid).toBe(false); expect(snap.legacyIssues).toHaveLength(0); - expect(snap.issues).toHaveLength(0); + expect(snap.issues.some((issue) => issue.path === "gateway.bind")).toBe(true); } finally { if (prev === undefined) { delete process.env.OPENCLAW_BIND; @@ -989,15 +986,16 @@ describe("config strict validation", () => { }); }); - it("still marks literal gateway.bind host aliases as legacy", async () => { + it("rejects literal gateway.bind host aliases as legacy", async () => { await withTempHome(async (home) => { await writeOpenClawConfig(home, { gateway: { bind: "0.0.0.0" }, }); const snap = await readConfigFileSnapshot(); - expect(snap.valid).toBe(true); - expect(snap.legacyIssues.some((issue) => issue.path === "gateway.bind")).toBe(true); + expect(snap.valid).toBe(false); + expect(snap.issues.some((issue) => issue.path === "gateway.bind")).toBe(true); + expect(snap.legacyIssues).toEqual([]); }); }); }); diff --git a/src/config/io.ts b/src/config/io.ts index 09c6f4ed1d4..b553f653e5b 100644 --- a/src/config/io.ts +++ b/src/config/io.ts @@ -5,7 +5,6 @@ import path from "node:path"; import JSON5 from "json5"; import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent-scope-config.js"; import { ensureOwnerDisplaySecret } from "../agents/owner-display.js"; -import { applyRuntimeLegacyConfigMigrations } from "../commands/doctor/shared/runtime-compat-api.js"; import { loadDotEnv } from "../infra/dotenv.js"; import { formatErrorMessage } from "../infra/errors.js"; import { resolveRequiredHomeDir } from "../infra/home-dir.js"; @@ -15,10 +14,6 @@ import { shouldDeferShellEnvFallback, shouldEnableShellEnvFallback, } from "../infra/shell-env.js"; -import { - collectRelevantDoctorPluginIds, - listPluginDoctorLegacyConfigRules, -} from "../plugins/doctor-contract-registry.js"; import { loadInstalledPluginIndexInstallRecordsSync, resolveInstalledPluginIndexRecordsStorePath, @@ -74,7 +69,6 @@ import { resolveManagedUnsetPathsForWrite, resolveWriteEnvSnapshotForPath, } from "./io.write-prepare.js"; -import { findLegacyConfigIssues } from "./legacy.js"; import { asResolvedSourceConfig, asRuntimeConfig, @@ -1069,14 +1063,11 @@ async function recoverConfigFromJsonRootSuffixWithDeps(params: { return false; } const readResolution = resolveConfigForRead(resolved, params.deps.env); - const legacyResolution = resolveLegacyConfigForRead( - readResolution.resolvedConfigRaw, - suffixRecovery.parsed, - ); const validated = validateConfigObjectWithPlugins( - stripShippedPluginInstallConfigRecords(legacyResolution.effectiveConfigRaw), + stripShippedPluginInstallConfigRecords(readResolution.resolvedConfigRaw), { env: params.deps.env, + sourceRaw: suffixRecovery.parsed, }, ); if (!validated.ok) { @@ -1098,11 +1089,6 @@ type ConfigReadResolution = { envWarnings: EnvSubstitutionWarning[]; }; -type LegacyMigrationResolution = { - effectiveConfigRaw: unknown; - sourceLegacyIssues: LegacyConfigIssue[]; -}; - function resolveConfigIncludesForRead( parsed: unknown, configPath: string, @@ -1148,29 +1134,6 @@ function resolveConfigForRead( }; } -function resolveLegacyConfigForRead( - resolvedConfigRaw: unknown, - sourceRaw: unknown, -): LegacyMigrationResolution { - const pluginIds = collectRelevantDoctorPluginIds(resolvedConfigRaw); - const sourceLegacyIssues = findLegacyConfigIssues( - resolvedConfigRaw, - sourceRaw, - listPluginDoctorLegacyConfigRules({ pluginIds }), - ); - if (!resolvedConfigRaw || typeof resolvedConfigRaw !== "object") { - return { - effectiveConfigRaw: resolvedConfigRaw, - sourceLegacyIssues, - }; - } - const compat = applyRuntimeLegacyConfigMigrations(resolvedConfigRaw); - return { - effectiveConfigRaw: compat.next ?? resolvedConfigRaw, - sourceLegacyIssues, - }; -} - type ReadConfigFileSnapshotInternalResult = { snapshot: ConfigFileSnapshot; envSnapshotForRestore?: Record; @@ -1527,11 +1490,9 @@ export function createConfigIO( deps.env, ); const resolvedConfig = readResolution.resolvedConfigRaw; - const legacyResolution = resolveLegacyConfigForRead(resolvedConfig, effectiveParsed); - const installMigration = migrateAndStripShippedPluginInstallConfigRecords( - legacyResolution.effectiveConfigRaw, - { rootConfigRaw: effectiveParsed }, - ); + const installMigration = migrateAndStripShippedPluginInstallConfigRecords(resolvedConfig, { + rootConfigRaw: effectiveParsed, + }); const effectiveConfigRaw = installMigration.config; const snapshotRaw = installMigration.persistedRootRaw ?? effectiveRaw; const snapshotParsed = installMigration.persistedRootParsed ?? effectiveParsed; @@ -1555,7 +1516,7 @@ export function createConfigIO( hash, issues: [], warnings: [], - legacyIssues: legacyResolution.sourceLegacyIssues, + legacyIssues: [], }), }); return {}; @@ -1570,6 +1531,7 @@ export function createConfigIO( const validated = validateConfigObjectWithPlugins(effectiveConfigRaw, { env: deps.env, pluginValidation: overrides.pluginValidation, + sourceRaw: effectiveParsed, }); if (!validated.ok) { observeLoadConfigSnapshot({ @@ -1584,7 +1546,7 @@ export function createConfigIO( hash, issues: validated.issues, warnings: validated.warnings, - legacyIssues: legacyResolution.sourceLegacyIssues, + legacyIssues: [], }), }); throwInvalidConfig({ @@ -1617,7 +1579,7 @@ export function createConfigIO( hash, issues: [], warnings: validated.warnings, - legacyIssues: legacyResolution.sourceLegacyIssues, + legacyIssues: [], }), }); return finalizeLoadedRuntimeConfig(cfg); @@ -1637,7 +1599,9 @@ export function createConfigIO( } async function readConfigFileSnapshotInternal( - options: { persistShippedPluginInstallMigration?: boolean } = {}, + options: { + persistShippedPluginInstallMigration?: boolean; + } = {}, ): Promise { maybeLoadDotEnvForConfig(deps.env); const exists = deps.fs.existsSync(configPath); @@ -1755,13 +1719,10 @@ export function createConfigIO( })); const resolvedConfigRaw = readResolution.resolvedConfigRaw; - const legacyResolution = await deps.measure("config.snapshot.read.legacy", () => - resolveLegacyConfigForRead(resolvedConfigRaw, effectiveParsed), - ); const installMigration = await deps.measure( "config.snapshot.read.plugin-install-migration", () => - migrateAndStripShippedPluginInstallConfigRecords(legacyResolution.effectiveConfigRaw, { + migrateAndStripShippedPluginInstallConfigRecords(resolvedConfigRaw, { persist: options.persistShippedPluginInstallMigration !== false, rootConfigRaw: effectiveParsed, }), @@ -1791,6 +1752,7 @@ export function createConfigIO( env: deps.env, pluginValidation: overrides.pluginValidation, loadPluginMetadataSnapshot: loadValidationPluginMetadataSnapshot, + sourceRaw: effectiveParsed, }), ); if (!validated.ok) { @@ -1806,7 +1768,7 @@ export function createConfigIO( hash: snapshotHash, issues: validated.issues, warnings: [...validated.warnings, ...envVarWarnings], - legacyIssues: legacyResolution.sourceLegacyIssues, + legacyIssues: [], }), }); } @@ -1830,7 +1792,7 @@ export function createConfigIO( hash: snapshotHash, issues: [], warnings: [...validated.warnings, ...envVarWarnings], - legacyIssues: legacyResolution.sourceLegacyIssues, + legacyIssues: [], }), envSnapshotForRestore: readResolution.envSnapshotForRestore, pluginMetadataSnapshot, @@ -1971,13 +1933,7 @@ export function createConfigIO( } const readResolution = resolveConfigForRead(resolved, deps.env); - const legacyResolution = resolveLegacyConfigForRead( - readResolution.resolvedConfigRaw, - recovered.parsed, - ); - return coerceConfig( - stripShippedPluginInstallConfigRecords(legacyResolution.effectiveConfigRaw), - ); + return coerceConfig(stripShippedPluginInstallConfigRecords(readResolution.resolvedConfigRaw)); } catch { return {}; } diff --git a/src/config/validation.ts b/src/config/validation.ts index 47e04eb3cb0..1cb21901efa 100644 --- a/src/config/validation.ts +++ b/src/config/validation.ts @@ -617,6 +617,7 @@ function validateGatewayTailscaleBind(config: OpenClawConfig): ConfigValidationI export function validateConfigObjectRaw( raw: unknown, opts?: { + sourceRaw?: unknown; touchedPaths?: ReadonlyArray>; }, ): { ok: true; config: OpenClawConfig } | { ok: false; issues: ConfigValidationIssue[] } { @@ -633,7 +634,7 @@ export function validateConfigObjectRaw( }); const legacyIssues = findLegacyConfigIssues( normalizedRaw, - normalizedRaw, + opts?.sourceRaw ?? normalizedRaw, extraLegacyRules, opts?.touchedPaths, ); @@ -686,8 +687,11 @@ export function validateConfigObjectRaw( export function validateConfigObject( raw: unknown, + opts?: { + sourceRaw?: unknown; + }, ): { ok: true; config: OpenClawConfig } | { ok: false; issues: ConfigValidationIssue[] } { - const result = validateConfigObjectRaw(raw); + const result = validateConfigObjectRaw(raw, opts); if (!result.ok) { return result; } @@ -716,6 +720,7 @@ type ValidateConfigWithPluginsParams = { loadPluginMetadataSnapshot?: ( config: OpenClawConfig, ) => Pick; + sourceRaw?: unknown; }; export function validateConfigObjectWithPlugins( @@ -728,6 +733,7 @@ export function validateConfigObjectWithPlugins( pluginValidation: params?.pluginValidation ?? "full", pluginMetadataSnapshot: params?.pluginMetadataSnapshot, loadPluginMetadataSnapshot: params?.loadPluginMetadataSnapshot, + sourceRaw: params?.sourceRaw, }); } @@ -741,6 +747,7 @@ export function validateConfigObjectRawWithPlugins( pluginValidation: params?.pluginValidation ?? "full", pluginMetadataSnapshot: params?.pluginMetadataSnapshot, loadPluginMetadataSnapshot: params?.loadPluginMetadataSnapshot, + sourceRaw: params?.sourceRaw, }); } @@ -748,7 +755,9 @@ function validateConfigObjectWithPluginsBase( raw: unknown, opts: ValidateConfigWithPluginsParams & { applyDefaults: boolean }, ): ValidateConfigWithPluginsResult { - const base = opts.applyDefaults ? validateConfigObject(raw) : validateConfigObjectRaw(raw); + const base = opts.applyDefaults + ? validateConfigObject(raw, { sourceRaw: opts.sourceRaw }) + : validateConfigObjectRaw(raw, { sourceRaw: opts.sourceRaw }); if (!base.ok) { return { ok: false, issues: base.issues, warnings: [] }; }