diff --git a/src/agents/agent-command.ts b/src/agents/agent-command.ts index 1ea6946eb96..0fee1f5efac 100644 --- a/src/agents/agent-command.ts +++ b/src/agents/agent-command.ts @@ -11,13 +11,8 @@ import { type VerboseLevel, } from "../auto-reply/thinking.js"; import { formatCliCommand } from "../cli/command-format.js"; -import { getAgentRuntimeCommandSecretTargetIds } from "../cli/command-secret-targets.js"; import { type CliDeps, createDefaultDeps } from "../cli/deps.js"; -import { loadConfig, readConfigFileSnapshotForWrite } from "../config/io.js"; -import { setRuntimeConfigSnapshot } from "../config/runtime-snapshot.js"; import type { SessionEntry } from "../config/sessions/types.js"; -import type { OpenClawConfig } from "../config/types.openclaw.js"; -import { isSecretRef } from "../config/types.secrets.js"; import { clearAgentRunContext, emitAgentEvent, @@ -36,6 +31,7 @@ import { resolveSendPolicy } from "../sessions/send-policy.js"; import { normalizeOptionalString } from "../shared/string-coerce.js"; import { sanitizeForLog } from "../terminal/ansi.js"; import { resolveMessageChannel } from "../utils/message-channel.js"; +import { resolveAgentRuntimeConfig } from "./agent-runtime-config.js"; import { listAgentIds, resolveAgentDir, @@ -145,101 +141,6 @@ async function persistSessionEntry(params: PersistSessionEntryParams): Promise { - const loadedRaw = loadConfig(); - const sourceConfig = await (async () => { - try { - const { snapshot } = await readConfigFileSnapshotForWrite(); - if (snapshot.valid) { - return snapshot.resolved; - } - } catch { - // Fall back to runtime-loaded config when source snapshot is unavailable. - } - return loadedRaw; - })(); - const includeChannelTargets = params?.runtimeTargetsChannelSecrets === true; - const cfg = hasAgentRuntimeSecretRefs({ - config: loadedRaw, - includeChannelTargets, - }) - ? ( - await ( - await import("../cli/command-config-resolution.runtime.js") - ).resolveCommandConfigWithSecrets({ - config: loadedRaw, - commandName: "agent", - targetIds: getAgentRuntimeCommandSecretTargetIds({ - includeChannelTargets, - }), - runtime, - }) - ).resolvedConfig - : loadedRaw; - setRuntimeConfigSnapshot(cfg, sourceConfig); - return { loadedRaw, sourceConfig, cfg }; -} - -function hasNestedSecretRef(value: unknown): boolean { - if (isSecretRef(value)) { - return true; - } - if (Array.isArray(value)) { - return value.some((entry) => hasNestedSecretRef(entry)); - } - if (!value || typeof value !== "object") { - return false; - } - return Object.values(value).some((entry) => hasNestedSecretRef(entry)); -} - -function hasAgentRuntimeSecretRefs(params: { - config: OpenClawConfig; - includeChannelTargets: boolean; -}): boolean { - const { config } = params; - if (hasNestedSecretRef(config.models?.providers)) { - return true; - } - if (hasNestedSecretRef(config.agents?.defaults?.memorySearch?.remote?.apiKey)) { - return true; - } - if ( - Array.isArray(config.agents?.list) && - config.agents.list.some((agent) => hasNestedSecretRef(agent?.memorySearch?.remote?.apiKey)) - ) { - return true; - } - if (hasNestedSecretRef(config.messages?.tts?.providers)) { - return true; - } - if (hasNestedSecretRef(config.skills?.entries)) { - return true; - } - if (hasNestedSecretRef(config.tools?.web?.search)) { - return true; - } - if ( - config.plugins?.entries && - Object.values(config.plugins.entries).some((entry) => - hasNestedSecretRef({ - webSearch: entry?.config?.webSearch, - webFetch: entry?.config?.webFetch, - }), - ) - ) { - return true; - } - return params.includeChannelTargets ? hasNestedSecretRef(config.channels) : false; -} - function containsControlCharacters(value: string): boolean { for (const char of value) { const code = char.codePointAt(0); diff --git a/src/agents/agent-runtime-config.ts b/src/agents/agent-runtime-config.ts new file mode 100644 index 00000000000..4dc7313be0f --- /dev/null +++ b/src/agents/agent-runtime-config.ts @@ -0,0 +1,101 @@ +import { getAgentRuntimeCommandSecretTargetIds } from "../cli/command-secret-targets.js"; +import { loadConfig, readConfigFileSnapshotForWrite } from "../config/io.js"; +import { setRuntimeConfigSnapshot } from "../config/runtime-snapshot.js"; +import type { OpenClawConfig } from "../config/types.openclaw.js"; +import { isSecretRef } from "../config/types.secrets.js"; +import type { RuntimeEnv } from "../runtime.js"; + +export async function resolveAgentRuntimeConfig( + runtime: RuntimeEnv, + params?: { runtimeTargetsChannelSecrets?: boolean }, +): Promise<{ + loadedRaw: OpenClawConfig; + sourceConfig: OpenClawConfig; + cfg: OpenClawConfig; +}> { + const loadedRaw = loadConfig(); + const sourceConfig = await (async () => { + try { + const { snapshot } = await readConfigFileSnapshotForWrite(); + if (snapshot.valid) { + return snapshot.resolved; + } + } catch { + // Fall back to runtime-loaded config when source snapshot is unavailable. + } + return loadedRaw; + })(); + const includeChannelTargets = params?.runtimeTargetsChannelSecrets === true; + const cfg = hasAgentRuntimeSecretRefs({ + config: loadedRaw, + includeChannelTargets, + }) + ? ( + await ( + await import("../cli/command-config-resolution.runtime.js") + ).resolveCommandConfigWithSecrets({ + config: loadedRaw, + commandName: "agent", + targetIds: getAgentRuntimeCommandSecretTargetIds({ + includeChannelTargets, + }), + runtime, + }) + ).resolvedConfig + : loadedRaw; + setRuntimeConfigSnapshot(cfg, sourceConfig); + return { loadedRaw, sourceConfig, cfg }; +} + +function hasNestedSecretRef(value: unknown): boolean { + if (isSecretRef(value)) { + return true; + } + if (Array.isArray(value)) { + return value.some((entry) => hasNestedSecretRef(entry)); + } + if (!value || typeof value !== "object") { + return false; + } + return Object.values(value).some((entry) => hasNestedSecretRef(entry)); +} + +function hasAgentRuntimeSecretRefs(params: { + config: OpenClawConfig; + includeChannelTargets: boolean; +}): boolean { + const { config } = params; + if (hasNestedSecretRef(config.models?.providers)) { + return true; + } + if (hasNestedSecretRef(config.agents?.defaults?.memorySearch?.remote?.apiKey)) { + return true; + } + if ( + Array.isArray(config.agents?.list) && + config.agents.list.some((agent) => hasNestedSecretRef(agent?.memorySearch?.remote?.apiKey)) + ) { + return true; + } + if (hasNestedSecretRef(config.messages?.tts?.providers)) { + return true; + } + if (hasNestedSecretRef(config.skills?.entries)) { + return true; + } + if (hasNestedSecretRef(config.tools?.web?.search)) { + return true; + } + if ( + config.plugins?.entries && + Object.values(config.plugins.entries).some((entry) => + hasNestedSecretRef({ + webSearch: entry?.config?.webSearch, + webFetch: entry?.config?.webFetch, + }), + ) + ) { + return true; + } + return params.includeChannelTargets ? hasNestedSecretRef(config.channels) : false; +} diff --git a/src/commands/agent.runtime-config.test.ts b/src/commands/agent.runtime-config.test.ts index 31cf8bd6bc9..eac29d496eb 100644 --- a/src/commands/agent.runtime-config.test.ts +++ b/src/commands/agent.runtime-config.test.ts @@ -1,25 +1,21 @@ import path from "node:path"; import { beforeEach, describe, expect, it, vi } from "vitest"; -import "./agent-command.test-mocks.js"; -import "../cron/isolated-agent.mocks.js"; -import { __testing as agentCommandTesting } from "../agents/agent-command.js"; +import { withTempHome as withTempHomeBase } from "../../test/helpers/temp-home.js"; +import { resolveAgentRuntimeConfig } from "../agents/agent-runtime-config.js"; import { resolveSession } from "../agents/command/session.js"; import * as commandConfigResolutionRuntimeModule from "../cli/command-config-resolution.runtime.js"; import * as configIoModule from "../config/io.js"; import * as runtimeSnapshotModule from "../config/runtime-snapshot.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; -import { - mockSharedAgentCommandConfig, - resetSharedAgentCommandRuntimeState, - runtime, - withSharedAgentCommandTempHome, -} from "./agent-runtime-config.test-support.js"; +import type { RuntimeEnv } from "../runtime.js"; -vi.mock("../agents/command/session-store.runtime.js", () => { - return { - updateSessionStoreAfterAgentRun: vi.fn(async () => undefined), - }; -}); +const runtime: RuntimeEnv = { + log: vi.fn(), + error: vi.fn(), + exit: vi.fn(() => { + throw new Error("exit"); + }), +}; const configSpy = vi.spyOn(configIoModule, "loadConfig"); const readConfigFileSnapshotForWriteSpy = vi.spyOn( @@ -28,19 +24,31 @@ const readConfigFileSnapshotForWriteSpy = vi.spyOn( ); async function withTempHome(fn: (home: string) => Promise): Promise { - return withSharedAgentCommandTempHome("openclaw-agent-", fn); + return withTempHomeBase(fn, { prefix: "openclaw-agent-" }); } -function mockConfig( - home: string, - storePath: string, - agentOverrides?: Parameters[3], -) { - return mockSharedAgentCommandConfig(configSpy, home, storePath, agentOverrides); +function mockConfig(home: string, storePath: string): OpenClawConfig { + const cfg = { + agents: { + defaults: { + model: { primary: "anthropic/claude-opus-4-6" }, + models: { "anthropic/claude-opus-4-6": {} }, + workspace: path.join(home, "openclaw"), + }, + }, + session: { store: storePath, mainKey: "main" }, + } as OpenClawConfig; + configSpy.mockReturnValue(cfg); + return cfg; } beforeEach(() => { - resetSharedAgentCommandRuntimeState(readConfigFileSnapshotForWriteSpy); + vi.clearAllMocks(); + runtimeSnapshotModule.clearRuntimeConfigSnapshot(); + readConfigFileSnapshotForWriteSpy.mockResolvedValue({ + snapshot: { valid: false, resolved: {} as OpenClawConfig }, + writeOptions: {}, + } as Awaited>); }); describe("agentCommand runtime config", () => { @@ -109,7 +117,7 @@ describe("agentCommand runtime config", () => { diagnostics: [], }); - const prepared = await agentCommandTesting.resolveAgentRuntimeConfig(runtime); + const prepared = await resolveAgentRuntimeConfig(runtime); expect(resolveConfigWithSecretsSpy).toHaveBeenCalledWith({ config: loadedConfig, @@ -144,7 +152,7 @@ describe("agentCommand runtime config", () => { diagnostics: [], }); - await agentCommandTesting.resolveAgentRuntimeConfig(runtime, { + await resolveAgentRuntimeConfig(runtime, { runtimeTargetsChannelSecrets: true, }); @@ -162,7 +170,7 @@ describe("agentCommand runtime config", () => { "resolveCommandConfigWithSecrets", ); - const prepared = await agentCommandTesting.resolveAgentRuntimeConfig(runtime); + const prepared = await resolveAgentRuntimeConfig(runtime); expect(resolveConfigWithSecretsSpy).not.toHaveBeenCalled(); expect(prepared.cfg).toBe(loadedConfig);