mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:00:43 +00:00
fix: keep legacy config repair in doctor
This commit is contained in:
@@ -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"]);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
31
src/commands/doctor-config-preflight.test.ts
Normal file
31
src/commands/doctor-config-preflight.test.ts
Normal 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",
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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.";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export { getModelsCommandSecretTargetIds } from "../../cli/command-secret-targets.js";
|
||||
export {
|
||||
getRuntimeConfig,
|
||||
readSourceConfigSnapshotForWrite,
|
||||
getRuntimeConfigSourceSnapshot,
|
||||
setRuntimeConfigSnapshot,
|
||||
type OpenClawConfig,
|
||||
} from "../../config/config.js";
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 {};
|
||||
}
|
||||
|
||||
@@ -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: [] };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user