diff --git a/src/agents/agent-command.ts b/src/agents/agent-command.ts index ee61e531423..5ff7e8e1a85 100644 --- a/src/agents/agent-command.ts +++ b/src/agents/agent-command.ts @@ -10,7 +10,6 @@ import { supportsXHighThinking, type VerboseLevel, } from "../auto-reply/thinking.js"; -import { resolveCommandConfigWithSecrets } from "../cli/command-config-resolution.js"; import { formatCliCommand } from "../cli/command-format.js"; import { getAgentRuntimeCommandSecretTargetIds } from "../cli/command-secret-targets.js"; import { type CliDeps, createDefaultDeps } from "../cli/deps.js"; @@ -19,6 +18,7 @@ import { setRuntimeConfigSnapshot } from "../config/runtime-snapshot.js"; import { resolveSessionTranscriptFile } from "../config/sessions/transcript.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, @@ -148,18 +148,81 @@ async function resolveAgentRuntimeConfig( } return loadedRaw; })(); - const { resolvedConfig: cfg } = await resolveCommandConfigWithSecrets({ + const includeChannelTargets = params?.runtimeTargetsChannelSecrets === true; + const cfg = hasAgentRuntimeSecretRefs({ config: loadedRaw, - commandName: "agent", - targetIds: getAgentRuntimeCommandSecretTargetIds({ - includeChannelTargets: params?.runtimeTargetsChannelSecrets === true, - }), - runtime, - }); + 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/cli/command-config-resolution.runtime.ts b/src/cli/command-config-resolution.runtime.ts new file mode 100644 index 00000000000..776b05b4c67 --- /dev/null +++ b/src/cli/command-config-resolution.runtime.ts @@ -0,0 +1 @@ +export { resolveCommandConfigWithSecrets } from "./command-config-resolution.js"; diff --git a/src/commands/agent.runtime-config.test.ts b/src/commands/agent.runtime-config.test.ts index 2464ec12adc..5cb67b4686b 100644 --- a/src/commands/agent.runtime-config.test.ts +++ b/src/commands/agent.runtime-config.test.ts @@ -4,7 +4,7 @@ import "./agent-command.test-mocks.js"; import "../cron/isolated-agent.mocks.js"; import { __testing as agentCommandTesting } from "../agents/agent-command.js"; import { resolveSession } from "../agents/command/session.js"; -import * as commandConfigResolutionModule from "../cli/command-config-resolution.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"; @@ -102,7 +102,7 @@ describe("agentCommand runtime config", () => { writeOptions: {}, } as Awaited>); const resolveConfigWithSecretsSpy = vi - .spyOn(commandConfigResolutionModule, "resolveCommandConfigWithSecrets") + .spyOn(commandConfigResolutionRuntimeModule, "resolveCommandConfigWithSecrets") .mockResolvedValueOnce({ resolvedConfig, effectiveConfig: resolvedConfig, @@ -131,8 +131,13 @@ describe("agentCommand runtime config", () => { await withTempHome(async (home) => { const store = path.join(home, "sessions.json"); const loadedConfig = mockConfig(home, store); + loadedConfig.channels = { + telegram: { + botToken: { source: "env", provider: "default", id: "TELEGRAM_BOT_TOKEN" }, + }, + } as OpenClawConfig["channels"]; const resolveConfigWithSecretsSpy = vi - .spyOn(commandConfigResolutionModule, "resolveCommandConfigWithSecrets") + .spyOn(commandConfigResolutionRuntimeModule, "resolveCommandConfigWithSecrets") .mockResolvedValueOnce({ resolvedConfig: loadedConfig, effectiveConfig: loadedConfig, @@ -148,6 +153,22 @@ describe("agentCommand runtime config", () => { }); }); + it("skips command secret resolution when no relevant SecretRef values exist", async () => { + await withTempHome(async (home) => { + const store = path.join(home, "sessions.json"); + const loadedConfig = mockConfig(home, store); + const resolveConfigWithSecretsSpy = vi.spyOn( + commandConfigResolutionRuntimeModule, + "resolveCommandConfigWithSecrets", + ); + + const prepared = await agentCommandTesting.resolveAgentRuntimeConfig(runtime); + + expect(resolveConfigWithSecretsSpy).not.toHaveBeenCalled(); + expect(prepared.cfg).toBe(loadedConfig); + }); + }); + it("derives a fresh session from --to", async () => { await withTempHome(async (home) => { const store = path.join(home, "sessions.json");