mirror of
https://github.com/openclaw/openclaw.git
synced 2026-02-19 18:39:20 -05:00
fix(config): add resolved field to ConfigFileSnapshot for pre-defaults config
The initial fix using snapshot.parsed broke configs with $include directives.
This commit adds a new 'resolved' field to ConfigFileSnapshot that contains
the config after $include and ${ENV} substitution but BEFORE runtime defaults
are applied. This is now used by config set/unset to avoid:
1. Breaking configs with $include directives
2. Leaking runtime defaults into the written config file
Also removes applyModelDefaults from writeConfigFile since runtime defaults
should only be applied when loading, not when writing.
This commit is contained in:
committed by
Peter Steinberger
parent
9e8d9f114d
commit
3189e2f11b
@@ -306,9 +306,10 @@ export function registerConfigCli(program: Command) {
|
||||
}
|
||||
const parsedValue = parseValue(value, opts);
|
||||
const snapshot = await loadValidConfig();
|
||||
// Use snapshot.parsed (raw user config) instead of snapshot.config (runtime-merged with defaults)
|
||||
// Use snapshot.resolved (config after $include and ${ENV} resolution, but BEFORE runtime defaults)
|
||||
// instead of snapshot.config (runtime-merged with defaults).
|
||||
// This prevents runtime defaults from leaking into the written config file (issue #6070)
|
||||
const next = structuredClone(snapshot.parsed) as Record<string, unknown>;
|
||||
const next = structuredClone(snapshot.resolved) as Record<string, unknown>;
|
||||
setAtPath(next, parsedPath, parsedValue);
|
||||
await writeConfigFile(next);
|
||||
defaultRuntime.log(info(`Updated ${path}. Restart the gateway to apply.`));
|
||||
@@ -329,9 +330,10 @@ export function registerConfigCli(program: Command) {
|
||||
throw new Error("Path is empty.");
|
||||
}
|
||||
const snapshot = await loadValidConfig();
|
||||
// Use snapshot.parsed (raw user config) instead of snapshot.config (runtime-merged with defaults)
|
||||
// Use snapshot.resolved (config after $include and ${ENV} resolution, but BEFORE runtime defaults)
|
||||
// instead of snapshot.config (runtime-merged with defaults).
|
||||
// This prevents runtime defaults from leaking into the written config file (issue #6070)
|
||||
const next = structuredClone(snapshot.parsed) as Record<string, unknown>;
|
||||
const next = structuredClone(snapshot.resolved) as Record<string, unknown>;
|
||||
const removed = unsetAtPath(next, parsedPath);
|
||||
if (!removed) {
|
||||
defaultRuntime.error(danger(`Config path not found: ${path}`));
|
||||
|
||||
@@ -10,5 +10,9 @@ export { migrateLegacyConfig } from "./legacy-migrate.js";
|
||||
export * from "./paths.js";
|
||||
export * from "./runtime-overrides.js";
|
||||
export * from "./types.js";
|
||||
export { validateConfigObject, validateConfigObjectWithPlugins } from "./validation.js";
|
||||
export {
|
||||
validateConfigObject,
|
||||
validateConfigObjectRaw,
|
||||
validateConfigObjectWithPlugins,
|
||||
} from "./validation.js";
|
||||
export { OpenClawSchema } from "./zod-schema.js";
|
||||
|
||||
@@ -353,6 +353,7 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) {
|
||||
exists: false,
|
||||
raw: null,
|
||||
parsed: {},
|
||||
resolved: {},
|
||||
valid: true,
|
||||
config,
|
||||
hash,
|
||||
@@ -372,6 +373,7 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) {
|
||||
exists: true,
|
||||
raw,
|
||||
parsed: {},
|
||||
resolved: {},
|
||||
valid: false,
|
||||
config: {},
|
||||
hash,
|
||||
@@ -398,6 +400,7 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) {
|
||||
exists: true,
|
||||
raw,
|
||||
parsed: parsedRes.parsed,
|
||||
resolved: coerceConfig(parsedRes.parsed),
|
||||
valid: false,
|
||||
config: coerceConfig(parsedRes.parsed),
|
||||
hash,
|
||||
@@ -426,6 +429,7 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) {
|
||||
exists: true,
|
||||
raw,
|
||||
parsed: parsedRes.parsed,
|
||||
resolved: coerceConfig(resolved),
|
||||
valid: false,
|
||||
config: coerceConfig(resolved),
|
||||
hash,
|
||||
@@ -445,6 +449,7 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) {
|
||||
exists: true,
|
||||
raw,
|
||||
parsed: parsedRes.parsed,
|
||||
resolved: coerceConfig(resolvedConfigRaw),
|
||||
valid: false,
|
||||
config: coerceConfig(resolvedConfigRaw),
|
||||
hash,
|
||||
@@ -460,6 +465,9 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) {
|
||||
exists: true,
|
||||
raw,
|
||||
parsed: parsedRes.parsed,
|
||||
// Use resolvedConfigRaw (after $include and ${ENV} substitution but BEFORE runtime defaults)
|
||||
// for config set/unset operations (issue #6070)
|
||||
resolved: coerceConfig(resolvedConfigRaw),
|
||||
valid: true,
|
||||
config: normalizeConfigPaths(
|
||||
applyTalkApiKey(
|
||||
@@ -481,6 +489,7 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) {
|
||||
exists: true,
|
||||
raw: null,
|
||||
parsed: {},
|
||||
resolved: {},
|
||||
valid: false,
|
||||
config: {},
|
||||
hash: hashConfigRaw(null),
|
||||
@@ -507,9 +516,9 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) {
|
||||
}
|
||||
const dir = path.dirname(configPath);
|
||||
await deps.fs.promises.mkdir(dir, { recursive: true, mode: 0o700 });
|
||||
const json = JSON.stringify(applyModelDefaults(stampConfigVersion(cfg)), null, 2)
|
||||
.trimEnd()
|
||||
.concat("\n");
|
||||
// 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 tmp = path.join(
|
||||
dir,
|
||||
|
||||
@@ -114,6 +114,12 @@ export type ConfigFileSnapshot = {
|
||||
exists: boolean;
|
||||
raw: string | null;
|
||||
parsed: unknown;
|
||||
/**
|
||||
* Config after $include resolution and ${ENV} substitution, but BEFORE runtime
|
||||
* defaults are applied. Use this for config set/unset operations to avoid
|
||||
* leaking runtime defaults into the written config file.
|
||||
*/
|
||||
resolved: OpenClawConfig;
|
||||
valid: boolean;
|
||||
config: OpenClawConfig;
|
||||
hash?: string;
|
||||
|
||||
@@ -83,7 +83,11 @@ function validateIdentityAvatar(config: OpenClawConfig): ConfigValidationIssue[]
|
||||
return issues;
|
||||
}
|
||||
|
||||
export function validateConfigObject(
|
||||
/**
|
||||
* Validates config without applying runtime defaults.
|
||||
* Use this when you need the raw validated config (e.g., for writing back to file).
|
||||
*/
|
||||
export function validateConfigObjectRaw(
|
||||
raw: unknown,
|
||||
): { ok: true; config: OpenClawConfig } | { ok: false; issues: ConfigValidationIssue[] } {
|
||||
const legacyIssues = findLegacyConfigIssues(raw);
|
||||
@@ -124,9 +128,20 @@ export function validateConfigObject(
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
config: applyModelDefaults(
|
||||
applyAgentDefaults(applySessionDefaults(validated.data as OpenClawConfig)),
|
||||
),
|
||||
config: validated.data as OpenClawConfig,
|
||||
};
|
||||
}
|
||||
|
||||
export function validateConfigObject(
|
||||
raw: unknown,
|
||||
): { ok: true; config: OpenClawConfig } | { ok: false; issues: ConfigValidationIssue[] } {
|
||||
const result = validateConfigObjectRaw(raw);
|
||||
if (!result.ok) {
|
||||
return result;
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
config: applyModelDefaults(applyAgentDefaults(applySessionDefaults(result.config))),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user