diff --git a/src/cli/gateway-cli/register.option-collisions.test.ts b/src/cli/gateway-cli/register.option-collisions.test.ts index c9ee93d6a2a..447da2955ee 100644 --- a/src/cli/gateway-cli/register.option-collisions.test.ts +++ b/src/cli/gateway-cli/register.option-collisions.test.ts @@ -72,6 +72,7 @@ vi.mock("../../commands/health.js", () => ({ vi.mock("../../config/read-best-effort-config.runtime.js", () => ({ readBestEffortConfig: async () => ({}), + readSourceConfigBestEffort: async () => ({}), })); vi.mock("../../infra/bonjour-discovery.js", () => ({ diff --git a/src/cli/gateway-cli/register.ts b/src/cli/gateway-cli/register.ts index d0f74c73937..1b454beca9c 100644 --- a/src/cli/gateway-cli/register.ts +++ b/src/cli/gateway-cli/register.ts @@ -265,7 +265,7 @@ export function registerGatewayCli(program: Command) { .action(async (opts: GatewayDiscoverOpts) => { await runGatewayCommand(async () => { const [ - { readBestEffortConfig }, + { readSourceConfigBestEffort }, { discoverGatewayBeacons }, { resolveWideAreaDiscoveryDomain }, ] = await Promise.all([ @@ -273,7 +273,7 @@ export function registerGatewayCli(program: Command) { loadBonjourDiscoveryModule(), loadWideAreaDnsModule(), ]); - const cfg = await readBestEffortConfig(); + const cfg = await readSourceConfigBestEffort(); const wideAreaDomain = resolveWideAreaDiscoveryDomain({ configDomain: cfg.discovery?.wideArea?.domain, }); diff --git a/src/config/config.ts b/src/config/config.ts index 70a52c1be99..f2eee8fc96d 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -10,6 +10,7 @@ export { projectConfigOntoRuntimeSourceSnapshot, loadConfig, readBestEffortConfig, + readSourceConfigBestEffort, parseConfigJson5, readConfigFileSnapshot, readConfigFileSnapshotForWrite, diff --git a/src/config/io.best-effort.test.ts b/src/config/io.best-effort.test.ts index accd9440c16..89993f9b91b 100644 --- a/src/config/io.best-effort.test.ts +++ b/src/config/io.best-effort.test.ts @@ -1,5 +1,9 @@ import { describe, expect, it } from "vitest"; -import { readBestEffortConfig, readConfigFileSnapshot } from "./config.js"; +import { + readBestEffortConfig, + readConfigFileSnapshot, + readSourceConfigBestEffort, +} from "./config.js"; import { withTempHome, writeOpenClawConfig } from "./test-helpers.js"; describe("readBestEffortConfig", () => { @@ -33,3 +37,29 @@ describe("readBestEffortConfig", () => { }); }); }); + +describe("readSourceConfigBestEffort", () => { + it("preserves the authored source config without load-time defaults", async () => { + await withTempHome(async (home) => { + await writeOpenClawConfig(home, { + auth: { + profiles: { + "anthropic:api": { provider: "anthropic", mode: "api_key" }, + }, + }, + agents: { + defaults: { + model: { primary: "anthropic/claude-opus-4-6" }, + }, + }, + }); + + const snapshot = await readConfigFileSnapshot(); + const sourceBestEffort = await readSourceConfigBestEffort(); + + expect(sourceBestEffort).toEqual(snapshot.resolved); + expect(sourceBestEffort.agents?.defaults?.contextPruning?.mode).toBeUndefined(); + expect(sourceBestEffort.agents?.defaults?.compaction?.mode).toBeUndefined(); + }); + }); +}); diff --git a/src/config/io.ts b/src/config/io.ts index ad2bab19a2a..5241474f25c 100644 --- a/src/config/io.ts +++ b/src/config/io.ts @@ -1413,6 +1413,45 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) { ); } + async function readSourceConfigBestEffort(): Promise { + maybeLoadDotEnvForConfig(deps.env); + const exists = deps.fs.existsSync(configPath); + if (!exists) { + return {}; + } + + try { + const raw = deps.fs.readFileSync(configPath, "utf-8"); + const parsedRes = parseConfigJson5(raw, deps.json5); + if (!parsedRes.ok) { + return {}; + } + + const recovered = await maybeRecoverSuspiciousConfigRead({ + deps, + configPath, + raw, + parsed: parsedRes.parsed, + }); + + let resolved: unknown; + try { + resolved = resolveConfigIncludesForRead(recovered.parsed, configPath, deps); + } catch { + return coerceConfig(recovered.parsed); + } + + const readResolution = resolveConfigForRead(resolved, deps.env); + const legacyResolution = resolveLegacyConfigForRead( + readResolution.resolvedConfigRaw, + recovered.parsed, + ); + return coerceConfig(legacyResolution.effectiveConfigRaw); + } catch { + return {}; + } + } + async function writeConfigFile( cfg: OpenClawConfig, options: ConfigWriteOptions = {}, @@ -1674,6 +1713,7 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) { configPath, loadConfig, readBestEffortConfig, + readSourceConfigBestEffort, readConfigFileSnapshot, readConfigFileSnapshotForWrite, writeConfigFile, @@ -1768,6 +1808,10 @@ export async function readBestEffortConfig(): Promise { return await createConfigIO().readBestEffortConfig(); } +export async function readSourceConfigBestEffort(): Promise { + return await createConfigIO().readSourceConfigBestEffort(); +} + export async function readConfigFileSnapshot(): Promise { return await createConfigIO().readConfigFileSnapshot(); } diff --git a/src/config/read-best-effort-config.runtime.ts b/src/config/read-best-effort-config.runtime.ts index ed2860a5b9b..c7103b0a252 100644 --- a/src/config/read-best-effort-config.runtime.ts +++ b/src/config/read-best-effort-config.runtime.ts @@ -1 +1 @@ -export { readBestEffortConfig } from "./io.js"; +export { readBestEffortConfig, readSourceConfigBestEffort } from "./io.js";