diff --git a/src/config/agent-list-projection.ts b/src/config/agent-list-projection.ts new file mode 100644 index 000000000000..995b15f831b9 --- /dev/null +++ b/src/config/agent-list-projection.ts @@ -0,0 +1,17 @@ +import { listAgentEntries } from "../agents/agent-scope.js"; +import type { OpenClawConfig } from "./types.js"; + +/** Attach the non-serialized list projection used by legacy runtime consumers. */ +export function attachAgentListProjection(config: OpenClawConfig): OpenClawConfig { + const agents = config.agents; + if (!agents || typeof agents !== "object" || Array.isArray(agents)) { + return config; + } + Object.defineProperty(agents, "list", { + configurable: true, + enumerable: false, + value: listAgentEntries(config), + writable: false, + }); + return config; +} diff --git a/src/config/runtime-overrides.test.ts b/src/config/runtime-overrides.test.ts index 5ab42c74d11d..2a0cf02ecdf4 100644 --- a/src/config/runtime-overrides.test.ts +++ b/src/config/runtime-overrides.test.ts @@ -1,5 +1,6 @@ // Covers runtime config overrides and precedence. import { beforeEach, describe, expect, it } from "vitest"; +import { listAgentWorkspaceDirs } from "../agents/workspace-dirs.js"; import { applyConfigOverrides, captureConfigOverrideApplier, @@ -8,7 +9,9 @@ import { setConfigOverride, unsetConfigOverride, } from "./runtime-overrides.js"; +import { resolveMainSessionKey } from "./sessions/main-session.js"; import type { OpenClawConfig } from "./types.js"; +import { validateConfigObject } from "./validation.js"; describe("runtime overrides", () => { beforeEach(() => { @@ -33,6 +36,44 @@ describe("runtime overrides", () => { expect(applyConfigOverrides({}).gateway?.auth?.token).toBe("later-token"); }); + it("preserves the validated agent projection when an override copies agents", () => { + const validated = validateConfigObject({ + agents: { + entries: { + jarvis: { + default: true, + workspace: "/tmp/jarvis-workspace", + }, + worker: { + workspace: "/tmp/worker-workspace", + }, + }, + }, + }); + if (!validated.ok) { + throw new Error("expected valid keyed agent config"); + } + + const override = setConfigOverride("agents.defaults.model", "test/model"); + expect(override.ok).toBe(true); + const applyCapturedOverrides = captureConfigOverrideApplier(); + const runtimeConfigs = [ + applyConfigOverrides(validated.config), + applyCapturedOverrides(validated.config), + ]; + + for (const runtimeConfig of runtimeConfigs) { + expect(runtimeConfig.agents).not.toBe(validated.config.agents); + expect(runtimeConfig.agents?.list?.map((entry) => entry.id)).toEqual(["jarvis", "worker"]); + expect(Object.keys(runtimeConfig.agents ?? {})).not.toContain("list"); + expect(listAgentWorkspaceDirs(runtimeConfig)).toEqual([ + "/tmp/jarvis-workspace", + "/tmp/worker-workspace", + ]); + expect(resolveMainSessionKey(runtimeConfig)).toBe("agent:jarvis:main"); + } + }); + it("merges object overrides without clobbering siblings", () => { const cfg = { channels: { whatsapp: { dmPolicy: "pairing", allowFrom: ["+1"] } }, diff --git a/src/config/runtime-overrides.ts b/src/config/runtime-overrides.ts index d44bf4ea0cc1..a68d07906da8 100644 --- a/src/config/runtime-overrides.ts +++ b/src/config/runtime-overrides.ts @@ -2,6 +2,7 @@ import { err, ok, type Result } from "@openclaw/normalization-core/result"; import { isBlockedObjectKey } from "../infra/prototype-keys.js"; // Applies runtime-only config overrides without mutating persisted config. import { isPlainObject } from "../utils.js"; +import { attachAgentListProjection } from "./agent-list-projection.js"; import { parseConfigPath, setConfigValueAtPath, unsetConfigValueAtPath } from "./config-paths.js"; import type { OpenClawConfig } from "./types.js"; @@ -46,6 +47,14 @@ function mergeOverrides(base: unknown, override: unknown): unknown { return next; } +function applyOverrideTree(cfg: OpenClawConfig, overrideTree: OverrideTree): OpenClawConfig { + const next = mergeOverrides(cfg, overrideTree) as OpenClawConfig; + if (next.agents === cfg.agents) { + return next; + } + return attachAgentListProjection(next); +} + /** Return the process-local runtime override tree used by debug config commands. */ export function getConfigOverrides(): OverrideTree { return overrides; @@ -81,7 +90,7 @@ export function applyConfigOverrides(cfg: OpenClawConfig): OpenClawConfig { if (!overrides || Object.keys(overrides).length === 0) { return cfg; } - return mergeOverrides(cfg, overrides) as OpenClawConfig; + return applyOverrideTree(cfg, overrides); } /** Capture an immutable applier for the process-local overrides active at this instant. */ @@ -90,5 +99,5 @@ export function captureConfigOverrideApplier(): (cfg: OpenClawConfig) => OpenCla if (Object.keys(capturedOverrides).length === 0) { return (cfg) => cfg; } - return (cfg) => mergeOverrides(cfg, capturedOverrides) as OpenClawConfig; + return (cfg) => applyOverrideTree(cfg, capturedOverrides); } diff --git a/src/config/validation-core.ts b/src/config/validation-core.ts index 1db01fc429f5..c469aed59759 100644 --- a/src/config/validation-core.ts +++ b/src/config/validation-core.ts @@ -3,7 +3,6 @@ import { isCanonicalDottedDecimalIPv4, isLoopbackIpAddress } from "@openclaw/net import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; import { sanitizeForLog } from "../../packages/terminal-core/src/ansi.js"; import { - listAgentEntries, listAgentEntriesWithSource, resolveAgentWorkspaceDir, resolveDefaultAgentId, @@ -22,6 +21,7 @@ import { } from "../shared/gateway-tailscale-auth-policy.js"; import { isRecord } from "../utils.js"; import { findDuplicateAgentDirs, formatDuplicateAgentDirError } from "./agent-dirs.js"; +import { attachAgentListProjection } from "./agent-list-projection.js"; import { migratePersistedImplicitMainRoster } from "./legacy.roster.js"; import { materializeRuntimeConfig } from "./materialize.js"; import { @@ -243,19 +243,6 @@ function collectModelPolicyAllowIssues(config: OpenClawConfig): ConfigValidation return issues; } -function attachAgentListProjection(config: OpenClawConfig): OpenClawConfig { - if (!config.agents) { - return config; - } - Object.defineProperty(config.agents, "list", { - configurable: true, - enumerable: false, - value: listAgentEntries(config), - writable: false, - }); - return config; -} - /** * Validates config without applying runtime defaults. * Use this when you need the raw validated config (e.g., for writing back to file).