mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 09:27:39 +00:00
fix(config): enforce default-free persistence in write path
This commit is contained in:
@@ -3,6 +3,7 @@ import crypto from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { isDeepStrictEqual } from "node:util";
|
||||
import type { OpenClawConfig, ConfigFileSnapshot, LegacyConfigIssue } from "./types.js";
|
||||
import { loadDotEnv } from "../infra/dotenv.js";
|
||||
import { resolveRequiredHomeDir } from "../infra/home-dir.js";
|
||||
@@ -28,10 +29,14 @@ import { MissingEnvVarError, resolveConfigEnvVars } from "./env-substitution.js"
|
||||
import { collectConfigEnvVars } from "./env-vars.js";
|
||||
import { ConfigIncludeError, resolveConfigIncludes } from "./includes.js";
|
||||
import { findLegacyConfigIssues } from "./legacy.js";
|
||||
import { applyMergePatch } from "./merge-patch.js";
|
||||
import { normalizeConfigPaths } from "./normalize-paths.js";
|
||||
import { resolveConfigPath, resolveDefaultConfigCandidates, resolveStateDir } from "./paths.js";
|
||||
import { applyConfigOverrides } from "./runtime-overrides.js";
|
||||
import { validateConfigObjectWithPlugins } from "./validation.js";
|
||||
import {
|
||||
validateConfigObjectRawWithPlugins,
|
||||
validateConfigObjectWithPlugins,
|
||||
} from "./validation.js";
|
||||
import { compareOpenClawVersions } from "./version.js";
|
||||
|
||||
// Re-export for backwards compatibility
|
||||
@@ -92,6 +97,49 @@ function coerceConfig(value: unknown): OpenClawConfig {
|
||||
return value as OpenClawConfig;
|
||||
}
|
||||
|
||||
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function cloneUnknown<T>(value: T): T {
|
||||
return structuredClone(value);
|
||||
}
|
||||
|
||||
function createMergePatch(base: unknown, target: unknown): unknown {
|
||||
if (!isPlainObject(base) || !isPlainObject(target)) {
|
||||
return cloneUnknown(target);
|
||||
}
|
||||
|
||||
const patch: Record<string, unknown> = {};
|
||||
const keys = new Set([...Object.keys(base), ...Object.keys(target)]);
|
||||
for (const key of keys) {
|
||||
const hasBase = key in base;
|
||||
const hasTarget = key in target;
|
||||
if (!hasTarget) {
|
||||
patch[key] = null;
|
||||
continue;
|
||||
}
|
||||
const targetValue = target[key];
|
||||
if (!hasBase) {
|
||||
patch[key] = cloneUnknown(targetValue);
|
||||
continue;
|
||||
}
|
||||
const baseValue = base[key];
|
||||
if (isPlainObject(baseValue) && isPlainObject(targetValue)) {
|
||||
const childPatch = createMergePatch(baseValue, targetValue);
|
||||
if (isPlainObject(childPatch) && Object.keys(childPatch).length === 0) {
|
||||
continue;
|
||||
}
|
||||
patch[key] = childPatch;
|
||||
continue;
|
||||
}
|
||||
if (!isDeepStrictEqual(baseValue, targetValue)) {
|
||||
patch[key] = cloneUnknown(targetValue);
|
||||
}
|
||||
}
|
||||
return patch;
|
||||
}
|
||||
|
||||
async function rotateConfigBackups(configPath: string, ioFs: typeof fs.promises): Promise<void> {
|
||||
if (CONFIG_BACKUP_COUNT <= 1) {
|
||||
return;
|
||||
@@ -502,7 +550,14 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) {
|
||||
|
||||
async function writeConfigFile(cfg: OpenClawConfig) {
|
||||
clearConfigCache();
|
||||
const validated = validateConfigObjectWithPlugins(cfg);
|
||||
let persistCandidate: unknown = cfg;
|
||||
const snapshot = await readConfigFileSnapshot();
|
||||
if (snapshot.valid && snapshot.exists) {
|
||||
const patch = createMergePatch(snapshot.config, cfg);
|
||||
persistCandidate = applyMergePatch(snapshot.resolved, patch);
|
||||
}
|
||||
|
||||
const validated = validateConfigObjectRawWithPlugins(persistCandidate);
|
||||
if (!validated.ok) {
|
||||
const issue = validated.issues[0];
|
||||
const pathLabel = issue?.path ? issue.path : "<root>";
|
||||
@@ -518,7 +573,9 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) {
|
||||
await deps.fs.promises.mkdir(dir, { recursive: true, mode: 0o700 });
|
||||
// Do NOT apply runtime defaults when writing — user config should only contain
|
||||
// explicitly set values. Runtime defaults are applied when loading (issue #6070).
|
||||
const json = JSON.stringify(stampConfigVersion(cfg), null, 2).trimEnd().concat("\n");
|
||||
const json = JSON.stringify(stampConfigVersion(validated.config), null, 2)
|
||||
.trimEnd()
|
||||
.concat("\n");
|
||||
|
||||
const tmp = path.join(
|
||||
dir,
|
||||
|
||||
Reference in New Issue
Block a user