fix: keep legacy config repair in doctor

This commit is contained in:
Peter Steinberger
2026-05-02 16:10:38 +01:00
parent 3f2c3a69d7
commit 5b063c2d83
10 changed files with 177 additions and 123 deletions

View File

@@ -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"]);

View File

@@ -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;
}

View File

@@ -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",
});
});
});
});

View File

@@ -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<ReturnType<typeof readConfigFileSnapshot>>,
): 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<ReturnType<typeof readConfigFileSnapshot>>,
): Awaited<ReturnType<typeof readConfigFileSnapshot>> {
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.";

View File

@@ -1,7 +1,7 @@
export { getModelsCommandSecretTargetIds } from "../../cli/command-secret-targets.js";
export {
getRuntimeConfig,
readSourceConfigSnapshotForWrite,
getRuntimeConfigSourceSnapshot,
setRuntimeConfigSnapshot,
type OpenClawConfig,
} from "../../config/config.js";

View File

@@ -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);
});
});

View File

@@ -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<OpenClawConfig> {
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<LoadedModelsConfig> {
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,

View File

@@ -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([]);
});
});
});

View File

@@ -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<string, string | undefined>;
@@ -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<ReadConfigFileSnapshotInternalResult> {
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 {};
}

View File

@@ -617,6 +617,7 @@ function validateGatewayTailscaleBind(config: OpenClawConfig): ConfigValidationI
export function validateConfigObjectRaw(
raw: unknown,
opts?: {
sourceRaw?: unknown;
touchedPaths?: ReadonlyArray<ReadonlyArray<string>>;
},
): { 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<PluginMetadataSnapshot, "manifestRegistry">;
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: [] };
}