refactor: share command config resolution

This commit is contained in:
Peter Steinberger
2026-04-06 07:40:23 +01:00
parent bb01e49192
commit f7833376ea
8 changed files with 150 additions and 46 deletions

View File

@@ -10,8 +10,8 @@ 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 { resolveCommandSecretRefsViaGateway } from "../cli/command-secret-gateway.js";
import { getAgentRuntimeCommandSecretTargetIds } from "../cli/command-secret-targets.js";
import { type CliDeps, createDefaultDeps } from "../cli/deps.js";
import {
@@ -178,10 +178,11 @@ async function prepareAgentCommandExecution(
}
return loadedRaw;
})();
const { resolvedConfig: cfg, diagnostics } = await resolveCommandSecretRefsViaGateway({
const { resolvedConfig: cfg } = await resolveCommandConfigWithSecrets({
config: loadedRaw,
commandName: "agent",
targetIds: getAgentRuntimeCommandSecretTargetIds(),
runtime,
});
setRuntimeConfigSnapshot(cfg, sourceConfig);
const normalizedSpawned = normalizeSpawnedRunMetadata({
@@ -191,9 +192,6 @@ async function prepareAgentCommandExecution(
groupSpace: opts.groupSpace,
workspaceDir: opts.workspaceDir,
});
for (const entry of diagnostics) {
runtime.log(`[secrets] ${entry}`);
}
const agentIdOverrideRaw = opts.agentId?.trim();
const agentIdOverride = agentIdOverrideRaw ? normalizeAgentId(agentIdOverrideRaw) : undefined;
if (agentIdOverride) {

View File

@@ -0,0 +1,82 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
const mocks = vi.hoisted(() => ({
resolveCommandSecretRefsViaGateway: vi.fn(),
applyPluginAutoEnable: vi.fn(),
}));
vi.mock("./command-secret-gateway.js", () => ({
resolveCommandSecretRefsViaGateway: mocks.resolveCommandSecretRefsViaGateway,
}));
vi.mock("../config/plugin-auto-enable.js", () => ({
applyPluginAutoEnable: mocks.applyPluginAutoEnable,
}));
import { resolveCommandConfigWithSecrets } from "./command-config-resolution.js";
describe("resolveCommandConfigWithSecrets", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("logs diagnostics and preserves resolved config when auto-enable is off", async () => {
const runtime = { log: vi.fn(), error: vi.fn(), exit: vi.fn() } as const;
const config = { channels: {} };
const resolvedConfig = { channels: { telegram: {} } };
const targetIds = new Set(["channels.telegram.token"]);
mocks.resolveCommandSecretRefsViaGateway.mockResolvedValue({
resolvedConfig,
diagnostics: ["resolved channels.telegram.token"],
});
const result = await resolveCommandConfigWithSecrets({
config,
commandName: "status",
targetIds,
mode: "read_only_status",
runtime,
});
expect(mocks.resolveCommandSecretRefsViaGateway).toHaveBeenCalledWith({
config,
commandName: "status",
targetIds,
mode: "read_only_status",
});
expect(runtime.log).toHaveBeenCalledWith("[secrets] resolved channels.telegram.token");
expect(mocks.applyPluginAutoEnable).not.toHaveBeenCalled();
expect(result).toEqual({
resolvedConfig,
effectiveConfig: resolvedConfig,
diagnostics: ["resolved channels.telegram.token"],
});
});
it("returns auto-enabled config when requested", async () => {
const resolvedConfig = { channels: {} };
const effectiveConfig = { channels: {}, plugins: { allow: ["telegram"] } };
mocks.resolveCommandSecretRefsViaGateway.mockResolvedValue({
resolvedConfig,
diagnostics: [],
});
mocks.applyPluginAutoEnable.mockReturnValue({
config: effectiveConfig,
changes: ["enabled telegram"],
});
const result = await resolveCommandConfigWithSecrets({
config: resolvedConfig,
commandName: "message",
targetIds: new Set(["channels.telegram.token"]),
autoEnable: true,
env: { OPENCLAW_AUTO_ENABLE: "1" } as NodeJS.ProcessEnv,
});
expect(mocks.applyPluginAutoEnable).toHaveBeenCalledWith({
config: resolvedConfig,
env: { OPENCLAW_AUTO_ENABLE: "1" },
});
expect(result.effectiveConfig).toBe(effectiveConfig);
});
});

View File

@@ -0,0 +1,46 @@
import { applyPluginAutoEnable } from "../config/plugin-auto-enable.js";
import type { OpenClawConfig } from "../config/types.js";
import type { RuntimeEnv } from "../runtime.js";
import {
type CommandSecretResolutionMode,
resolveCommandSecretRefsViaGateway,
} from "./command-secret-gateway.js";
export async function resolveCommandConfigWithSecrets<TConfig extends OpenClawConfig>(params: {
config: TConfig;
commandName: string;
targetIds: Set<string>;
mode?: CommandSecretResolutionMode;
allowedPaths?: Set<string>;
runtime?: RuntimeEnv;
autoEnable?: boolean;
env?: NodeJS.ProcessEnv;
}): Promise<{
resolvedConfig: TConfig;
effectiveConfig: TConfig;
diagnostics: string[];
}> {
const { resolvedConfig, diagnostics } = await resolveCommandSecretRefsViaGateway({
config: params.config,
commandName: params.commandName,
targetIds: params.targetIds,
...(params.mode ? { mode: params.mode } : {}),
...(params.allowedPaths ? { allowedPaths: params.allowedPaths } : {}),
});
if (params.runtime) {
for (const entry of diagnostics) {
params.runtime.log(`[secrets] ${entry}`);
}
}
const effectiveConfig = params.autoEnable
? applyPluginAutoEnable({
config: resolvedConfig,
env: params.env ?? process.env,
}).config
: resolvedConfig;
return {
resolvedConfig: resolvedConfig as TConfig,
effectiveConfig: effectiveConfig as TConfig,
diagnostics,
};
}

View File

@@ -1,9 +1,8 @@
import { getChannelPlugin } from "../../channels/plugins/index.js";
import type { ChannelResolveKind, ChannelResolveResult } from "../../channels/plugins/types.js";
import { resolveCommandSecretRefsViaGateway } from "../../cli/command-secret-gateway.js";
import { resolveCommandConfigWithSecrets } from "../../cli/command-config-resolution.js";
import { getChannelsCommandSecretTargetIds } from "../../cli/command-secret-targets.js";
import { loadConfig, readConfigFileSnapshot, replaceConfigFile } from "../../config/config.js";
import { applyPluginAutoEnable } from "../../config/plugin-auto-enable.js";
import { danger } from "../../globals.js";
import { resolveMessageChannelSelection } from "../../infra/outbound/channel-selection.js";
import { type RuntimeEnv, writeRuntimeJson } from "../../runtime.js";
@@ -108,19 +107,14 @@ function formatResolveResult(result: ResolveResult): string {
export async function channelsResolveCommand(opts: ChannelsResolveOptions, runtime: RuntimeEnv) {
const sourceSnapshotPromise = readConfigFileSnapshot().catch(() => null);
const loadedRaw = loadConfig();
const { resolvedConfig, diagnostics } = await resolveCommandSecretRefsViaGateway({
let { effectiveConfig: cfg } = await resolveCommandConfigWithSecrets({
config: loadedRaw,
commandName: "channels resolve",
targetIds: getChannelsCommandSecretTargetIds(),
mode: "read_only_operational",
runtime,
autoEnable: true,
});
let cfg = applyPluginAutoEnable({
config: resolvedConfig,
env: process.env,
}).config;
for (const entry of diagnostics) {
runtime.log(`[secrets] ${entry}`);
}
const entries = (opts.entries ?? []).map((entry) => entry.trim()).filter(Boolean);
if (entries.length === 0) {
throw new Error("At least one entry is required.");

View File

@@ -1,8 +1,6 @@
import { type ChannelId, getChannelPlugin } from "../../channels/plugins/index.js";
import {
type CommandSecretResolutionMode,
resolveCommandSecretRefsViaGateway,
} from "../../cli/command-secret-gateway.js";
import { resolveCommandConfigWithSecrets } from "../../cli/command-config-resolution.js";
import type { CommandSecretResolutionMode } from "../../cli/command-secret-gateway.js";
import { getChannelsCommandSecretTargetIds } from "../../cli/command-secret-targets.js";
import type { OpenClawConfig } from "../../config/config.js";
import { DEFAULT_ACCOUNT_ID } from "../../routing/session-key.js";
@@ -28,16 +26,14 @@ export async function requireValidConfig(
if (!cfg) {
return null;
}
const { resolvedConfig, diagnostics } = await resolveCommandSecretRefsViaGateway({
const { effectiveConfig } = await resolveCommandConfigWithSecrets({
config: cfg,
commandName: secretResolution?.commandName ?? "channels",
targetIds: getChannelsCommandSecretTargetIds(),
mode: secretResolution?.mode,
runtime,
});
for (const entry of diagnostics) {
runtime.log(`[secrets] ${entry}`);
}
return resolvedConfig;
return effectiveConfig;
}
export function formatAccountLabel(params: { accountId: string; name?: string }) {

View File

@@ -8,8 +8,8 @@ import {
buildReadOnlySourceChannelAccountSnapshot,
} from "../../channels/plugins/status.js";
import type { ChannelAccountSnapshot } from "../../channels/plugins/types.js";
import { resolveCommandConfigWithSecrets } from "../../cli/command-config-resolution.js";
import { formatCliCommand } from "../../cli/command-format.js";
import { resolveCommandSecretRefsViaGateway } from "../../cli/command-secret-gateway.js";
import { getChannelsCommandSecretTargetIds } from "../../cli/command-secret-targets.js";
import { withProgress } from "../../cli/progress.js";
import { type OpenClawConfig, readConfigFileSnapshot } from "../../config/config.js";
@@ -311,15 +311,13 @@ export async function channelsStatusCommand(
if (!cfg) {
return;
}
const { resolvedConfig, diagnostics } = await resolveCommandSecretRefsViaGateway({
const { resolvedConfig } = await resolveCommandConfigWithSecrets({
config: cfg,
commandName: "channels status",
targetIds: getChannelsCommandSecretTargetIds(),
mode: "read_only_status",
runtime,
});
for (const entry of diagnostics) {
runtime.log(`[secrets] ${entry}`);
}
const snapshot = await readConfigFileSnapshot();
const mode = cfg.gateway?.mode === "remote" ? "remote" : "local";
runtime.log(

View File

@@ -3,13 +3,12 @@ import {
CHANNEL_MESSAGE_ACTION_NAMES,
type ChannelMessageActionName,
} from "../channels/plugins/types.js";
import { resolveCommandSecretRefsViaGateway } from "../cli/command-secret-gateway.js";
import { resolveCommandConfigWithSecrets } from "../cli/command-config-resolution.js";
import { getScopedChannelsCommandSecretTargets } from "../cli/command-secret-targets.js";
import { resolveMessageSecretScope } from "../cli/message-secret-scope.js";
import { createOutboundSendDeps, type CliDeps } from "../cli/outbound-send-deps.js";
import { withProgress } from "../cli/progress.js";
import { loadConfig } from "../config/config.js";
import { applyPluginAutoEnable } from "../config/plugin-auto-enable.js";
import type { OutboundSendDeps } from "../infra/outbound/deliver.js";
import { runMessageAction } from "../infra/outbound/message-action-runner.js";
import { type RuntimeEnv, writeRuntimeJson } from "../runtime.js";
@@ -33,19 +32,14 @@ export async function messageCommand(
channel: scope.channel,
accountId: scope.accountId,
});
const { resolvedConfig, diagnostics } = await resolveCommandSecretRefsViaGateway({
const { effectiveConfig: cfg } = await resolveCommandConfigWithSecrets({
config: loadedRaw,
commandName: "message",
targetIds: scopedTargets.targetIds,
...(scopedTargets.allowedPaths ? { allowedPaths: scopedTargets.allowedPaths } : {}),
runtime,
autoEnable: true,
});
const cfg = applyPluginAutoEnable({
config: resolvedConfig,
env: process.env,
}).config;
for (const entry of diagnostics) {
runtime.log(`[secrets] ${entry}`);
}
const rawAction = typeof opts.action === "string" ? opts.action.trim() : "";
const actionInput = rawAction || "send";
const actionMatch = (CHANNEL_MESSAGE_ACTION_NAMES as readonly string[]).find(

View File

@@ -1,3 +1,4 @@
import { resolveCommandConfigWithSecrets } from "../../cli/command-config-resolution.js";
import type { RuntimeEnv } from "../../runtime.js";
import {
getRuntimeConfig,
@@ -5,7 +6,6 @@ import {
setRuntimeConfigSnapshot,
type OpenClawConfig,
getModelsCommandSecretTargetIds,
resolveCommandSecretRefsViaGateway,
} from "./load-config.runtime.js";
export type LoadedModelsConfig = {
@@ -32,16 +32,12 @@ export async function loadModelsConfigWithSource(params: {
}): Promise<LoadedModelsConfig> {
const runtimeConfig = getRuntimeConfig();
const sourceConfig = await loadSourceConfigSnapshot(runtimeConfig);
const { resolvedConfig, diagnostics } = await resolveCommandSecretRefsViaGateway({
const { resolvedConfig, diagnostics } = await resolveCommandConfigWithSecrets({
config: runtimeConfig,
commandName: params.commandName,
targetIds: getModelsCommandSecretTargetIds(),
runtime: params.runtime,
});
if (params.runtime) {
for (const entry of diagnostics) {
params.runtime.log(`[secrets] ${entry}`);
}
}
setRuntimeConfigSnapshot(resolvedConfig, sourceConfig);
return {
sourceConfig,