openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
784 lines (783 loc) • 33 kB
JavaScript
import { w as parseStrictPositiveInteger } from "./number-coercion-CLj0HTDM.js";
import { c as isRecord } from "./record-coerce-DItp3I4t.js";
import { c as normalizeOptionalLowercaseString, l as normalizeOptionalString, p as normalizeStringifiedOptionalString } from "./string-coerce-CIXf7egm.js";
import { a as normalizeCsvOrLooseStringList } from "./string-normalization-DsCfAx8q.js";
import { n as normalizeAgentId } from "./agent-id-CeT3w4ap.js";
import { o as listAgentIds, u as resolveAgentDir, y as resolveDefaultAgentId } from "./agent-scope-config-DcbEhP0R.js";
import "./session-key-BnWWjqNc.js";
import { t as parseConfigPathArrayIndex } from "./path-array-index-CvEcUJa-.js";
import { p as isValidEnvSecretRefId, v as resolveSecretInputRef } from "./types.secrets-kC0nOetj.js";
import { t as isSafeExecutableValue } from "./exec-safety-DtLGRBJm.js";
import { c as isValidSecretProviderAlias, i as formatExecSecretRefIdValidationMessage, o as isValidExecSecretRefId, u as resolveDefaultSecretProviderAlias } from "./ref-contract-D92DqQ-r.js";
import { n as loadPluginManifestRegistryCore } from "./manifest-registry-DCCgYk7q.js";
import "./agent-scope-DbtJyKUL.js";
import { t as getProviderEnvVars } from "./provider-env-vars-B8XgJCSL.js";
import "./shared-CeAc-MzA.js";
import { f as assertExpectedResolvedSecretValue } from "./runtime-shared-CqbaocQu.js";
import { n as discoverConfigSecretTargets, t as discoverAuthProfileSecretTargets } from "./target-registry-query-qVhyD-Hq.js";
import "./target-registry-BHDUOLUU.js";
import { n as resolveSecretRefValue, s as listSecretProviderIntegrationPresets } from "./resolve-224YoYfx.js";
import { a as loadPersistedAuthProfileStore } from "./persisted-B_qhhBlh.js";
import { t as getSkippedExecRefStaticError } from "./exec-resolution-policy-B4IeYZE3.js";
import { t as createSecretsConfigIO } from "./config-io-DIBeaezB.js";
import { t as runSecretsApply } from "./apply-CO2OX_Ur.js";
import { isDeepStrictEqual } from "node:util";
import path from "node:path";
import { confirm, select, text } from "@clack/prompts";
//#region src/secrets/configure-plan.ts
/** Builds the interactive `openclaw secrets configure` target list and apply plan. */
function getSecretProviders$1(config) {
if (!isRecord(config.secrets?.providers)) return {};
return config.secrets.providers;
}
function configureCandidateSortKey(candidate) {
if (candidate.configFile === "auth-profile-store") return `auth-profiles:${candidate.agentId ?? ""}:${candidate.path}`;
return `openclaw:${candidate.path}`;
}
function resolveAuthProfileProvider(store, pathSegments) {
const profileId = pathSegments[1];
if (!profileId) return;
const profile = store.profiles?.[profileId];
if (!isRecord(profile) || typeof profile.provider !== "string") return;
const provider = profile.provider.trim();
return provider.length > 0 ? provider : void 0;
}
/** Builds configure candidates for OpenClaw config plus an optional auth-profile scope. */
function buildConfigureCandidatesForScope(params) {
const authoredConfig = params.authoredOpenClawConfig ?? params.config;
const hasPathInAuthoredConfig = (pathSegments) => hasPath(authoredConfig, pathSegments);
const openclawCandidates = discoverConfigSecretTargets(params.config).filter((entry) => entry.entry.includeInConfigure).map((entry) => {
const resolved = resolveSecretInputRef({
value: entry.value,
refValue: entry.refValue,
defaults: params.config.secrets?.defaults
});
const pathExists = hasPathInAuthoredConfig(entry.pathSegments);
const refPathExists = entry.refPathSegments ? hasPathInAuthoredConfig(entry.refPathSegments) : false;
return Object.assign({
type: entry.entry.targetType,
path: entry.path,
pathSegments: [...entry.pathSegments],
label: entry.path,
configFile: `openclaw.json`,
expectedResolvedValue: entry.entry.expectedResolvedValue
}, resolved.ref ? { existingRef: resolved.ref } : {}, pathExists || refPathExists ? {} : { isDerived: true }, entry.providerId ? { providerId: entry.providerId } : {}, entry.accountId ? { accountId: entry.accountId } : {});
});
const authCandidates = params.authProfiles === void 0 ? [] : discoverAuthProfileSecretTargets(params.authProfiles.store).filter((entry) => entry.entry.includeInConfigure).map((entry) => {
const authProfiles = params.authProfiles;
if (!authProfiles) throw new Error("Missing auth profile scope for configure candidate discovery.");
const authProfileProvider = resolveAuthProfileProvider(authProfiles.store, entry.pathSegments);
const resolved = resolveSecretInputRef({
value: entry.value,
refValue: entry.refValue,
defaults: params.config.secrets?.defaults
});
return Object.assign({
type: entry.entry.targetType,
path: entry.path,
pathSegments: [...entry.pathSegments],
label: `${entry.path} (auth profile, agent ${authProfiles.agentId})`,
configFile: `auth-profile-store`,
expectedResolvedValue: entry.entry.expectedResolvedValue
}, resolved.ref ? { existingRef: resolved.ref } : {}, { agentId: authProfiles.agentId }, authProfileProvider ? { authProfileProvider } : {});
});
return [...openclawCandidates, ...authCandidates].toSorted((a, b) => configureCandidateSortKey(a).localeCompare(configureCandidateSortKey(b)));
}
function hasPath(root, segments) {
if (segments.length === 0) return false;
let cursor = root;
for (let index = 0; index < segments.length; index += 1) {
const segment = segments[index] ?? "";
if (Array.isArray(cursor)) {
const parsedIndex = parseConfigPathArrayIndex(segment);
if (parsedIndex === void 0 || parsedIndex >= cursor.length) return false;
if (index === segments.length - 1) return true;
cursor = cursor[parsedIndex];
continue;
}
if (!isRecord(cursor)) return false;
if (!Object.hasOwn(cursor, segment)) return false;
if (index === segments.length - 1) return true;
cursor = cursor[segment];
}
return false;
}
/** Computes provider upserts/deletes between original and edited config. */
function collectConfigureProviderChanges(params) {
const originalProviders = getSecretProviders$1(params.original);
const nextProviders = getSecretProviders$1(params.next);
const upserts = {};
const deletes = [];
for (const [providerAlias, nextProviderConfig] of Object.entries(nextProviders)) {
const current = originalProviders[providerAlias];
if (isDeepStrictEqual(current, nextProviderConfig)) continue;
upserts[providerAlias] = structuredClone(nextProviderConfig);
}
for (const providerAlias of Object.keys(originalProviders)) if (!Object.hasOwn(nextProviders, providerAlias)) deletes.push(providerAlias);
return {
upserts,
deletes: deletes.toSorted()
};
}
/** Returns true when selected targets or provider mutations would produce a plan. */
function hasConfigurePlanChanges(params) {
return params.selectedTargets.size > 0 || Object.keys(params.providerChanges.upserts).length > 0 || params.providerChanges.deletes.length > 0;
}
/** Builds the serializable secrets apply plan from configure selections. */
function buildSecretsConfigurePlan(params) {
return {
version: 1,
protocolVersion: 1,
generatedAt: params.generatedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
generatedBy: "openclaw secrets configure",
targets: [...params.selectedTargets.values()].map((entry) => Object.assign({
type: entry.type,
path: entry.path,
pathSegments: [...entry.pathSegments],
ref: entry.ref
}, entry.agentId ? { agentId: entry.agentId } : {}, entry.providerId ? { providerId: entry.providerId } : {}, entry.accountId ? { accountId: entry.accountId } : {}, entry.authProfileProvider ? { authProfileProvider: entry.authProfileProvider } : {})),
...Object.keys(params.providerChanges.upserts).length > 0 ? { providerUpserts: params.providerChanges.upserts } : {},
...params.providerChanges.deletes.length > 0 ? { providerDeletes: params.providerChanges.deletes } : {},
options: {
scrubEnv: true,
scrubAuthProfilesForProviderTargets: true,
scrubLegacyAuthJson: false
}
};
}
//#endregion
//#region src/secrets/configure.ts
/** Interactive and noninteractive secrets configure workflow. */
const WINDOWS_ABS_PATH_PATTERN = /^[A-Za-z]:[\\/]/;
const WINDOWS_UNC_PATH_PATTERN = /^\\\\[^\\]+\\[^\\]+/;
function isAbsolutePathValue(value) {
return path.isAbsolute(value) || WINDOWS_ABS_PATH_PATTERN.test(value) || WINDOWS_UNC_PATH_PATTERN.test(value);
}
function parseOptionalPositiveInt(value, max) {
const trimmed = value.trim();
if (!trimmed) return;
if (!/^\d+$/.test(trimmed)) return;
const parsed = parseStrictPositiveInteger(trimmed);
if (parsed === void 0 || parsed > max) return;
return parsed;
}
function getSecretProviders(config) {
if (!isRecord(config.secrets?.providers)) return {};
return config.secrets.providers;
}
function setSecretProvider(config, providerAlias, providerConfig) {
config.secrets ??= {};
if (!isRecord(config.secrets.providers)) config.secrets.providers = {};
config.secrets.providers[providerAlias] = providerConfig;
}
function removeSecretProvider(config, providerAlias) {
if (!isRecord(config.secrets?.providers)) return false;
const providers = config.secrets.providers;
if (!Object.hasOwn(providers, providerAlias)) return false;
delete providers[providerAlias];
if (Object.keys(providers).length === 0) delete config.secrets?.providers;
if (isRecord(config.secrets?.defaults)) {
const defaults = config.secrets.defaults;
if (defaults?.env === providerAlias) delete defaults.env;
if (defaults?.file === providerAlias) delete defaults.file;
if (defaults?.exec === providerAlias) delete defaults.exec;
if (defaults?.store === providerAlias) delete defaults.store;
if (defaults && defaults.env === void 0 && defaults.file === void 0 && defaults.exec === void 0 && defaults.store === void 0) delete config.secrets?.defaults;
}
return true;
}
function providerHint(provider) {
if (provider.source === "env") return provider.allowlist?.length ? `env (${provider.allowlist.length} allowlisted)` : "env";
if (provider.source === "file") return `file (${provider.mode ?? "json"})`;
if (provider.source === "store") return "store";
if ("pluginIntegration" in provider) {
const { pluginId, integrationId } = provider.pluginIntegration;
return `exec plugin (${pluginId}:${integrationId})`;
}
return `exec (${provider.jsonOnly === false ? "json+text" : "json"})`;
}
function providerPresetKey(preset) {
return `${preset.pluginId}:${preset.id}:${preset.providerAlias}`;
}
function providerPresetHint(preset) {
return `${preset.providerAlias} | ${preset.pluginId}:${preset.id} | exec plugin`;
}
function loadSecretProviderIntegrationPresets(params) {
const manifestRegistry = loadPluginManifestRegistryCore({
config: params.config,
env: params.env
});
return listSecretProviderIntegrationPresets({
manifestRegistry,
config: params.config,
env: params.env
});
}
function toSourceChoices(config) {
const hasSource = (source) => Object.values(config.secrets?.providers ?? {}).some((provider) => provider?.source === source);
const choices = [{
value: "env",
label: "env"
}, {
value: "store",
label: "store"
}];
if (hasSource("file")) choices.push({
value: "file",
label: "file"
});
if (hasSource("exec")) choices.push({
value: "exec",
label: "exec"
});
return choices;
}
function assertNoCancel(value, message) {
if (typeof value === "symbol") throw new Error(message);
return value;
}
const AUTH_PROFILE_ID_PATTERN = /^[A-Za-z0-9:_-]{1,128}$/;
function validateEnvNameCsv(value) {
const entries = normalizeCsvOrLooseStringList(value);
for (const entry of entries) if (!isValidEnvSecretRefId(entry)) return `Invalid env name: ${entry}`;
}
async function promptEnvNameCsv(params) {
const raw = assertNoCancel(await text({
message: params.message,
initialValue: params.initialValue,
validate: (value) => validateEnvNameCsv(value ?? "")
}), "Secrets configure cancelled.");
return normalizeCsvOrLooseStringList(raw ?? "");
}
async function promptOptionalPositiveInt(params) {
const raw = assertNoCancel(await text({
message: params.message,
initialValue: params.initialValue === void 0 ? "" : String(params.initialValue),
validate: (value) => {
const trimmed = normalizeStringifiedOptionalString(value) ?? "";
if (!trimmed) return;
if (parseOptionalPositiveInt(trimmed, params.max) === void 0) return `Must be an integer between 1 and ${params.max}`;
}
}), "Secrets configure cancelled.");
return parseOptionalPositiveInt(normalizeStringifiedOptionalString(raw) ?? "", params.max);
}
function configureCandidateKey(candidate) {
if (candidate.configFile === "auth-profile-store") return `auth-profiles:${normalizeOptionalString(candidate.agentId) ?? ""}:${candidate.path}`;
return `openclaw:${candidate.path}`;
}
function hasSourceChoice(sourceChoices, source) {
return sourceChoices.some((entry) => entry.value === source);
}
function resolveCandidateProviderHint(candidate) {
return normalizeOptionalLowercaseString(candidate.authProfileProvider) ?? normalizeOptionalLowercaseString(candidate.providerId);
}
function resolveSuggestedEnvSecretId(candidate) {
const hintedProvider = resolveCandidateProviderHint(candidate);
if (!hintedProvider) return;
const envCandidates = getProviderEnvVars(hintedProvider);
if (!Array.isArray(envCandidates) || envCandidates.length === 0) return;
return envCandidates[0];
}
function resolveConfigureAgentId(config, explicitAgentId) {
const knownAgentIds = new Set(listAgentIds(config));
if (!explicitAgentId) return resolveDefaultAgentId(config);
const normalized = normalizeAgentId(explicitAgentId);
if (knownAgentIds.has(normalized)) return normalized;
const known = [...knownAgentIds].toSorted().join(", ");
throw new Error(`Unknown agent id "${explicitAgentId}". Known agents: ${known || "none configured"}.`);
}
function loadAuthProfileStoreForConfigure(params) {
const agentDir = resolveAgentDir(params.config, params.agentId);
return loadPersistedAuthProfileStore(agentDir) ?? {
version: 1,
profiles: {}
};
}
async function promptNewAuthProfileCandidate(agentId) {
const profileId = assertNoCancel(await text({
message: "Auth profile id",
validate: (value) => {
const trimmed = normalizeStringifiedOptionalString(value) ?? "";
if (!trimmed) return "Required";
if (!AUTH_PROFILE_ID_PATTERN.test(trimmed)) return "Use letters/numbers/\":\"/\"_\"/\"-\" only.";
}
}), "Secrets configure cancelled.");
const credentialType = assertNoCancel(await select({
message: "Auth profile credential type",
options: [{
value: "api_key",
label: "api_key (key/keyRef)"
}, {
value: "token",
label: "token (token/tokenRef)"
}]
}), "Secrets configure cancelled.");
const provider = assertNoCancel(await text({
message: "Provider id",
validate: (value) => normalizeStringifiedOptionalString(value) ? void 0 : "Required"
}), "Secrets configure cancelled.");
const profileIdTrimmed = normalizeStringifiedOptionalString(profileId) ?? "";
const providerTrimmed = normalizeStringifiedOptionalString(provider) ?? "";
if (credentialType === "token") return {
type: "auth-profiles.token.token",
path: `profiles.${profileIdTrimmed}.token`,
pathSegments: [
"profiles",
profileIdTrimmed,
"token"
],
label: `profiles.${profileIdTrimmed}.token (auth profile, agent ${agentId})`,
configFile: "auth-profile-store",
agentId,
authProfileProvider: providerTrimmed,
expectedResolvedValue: "string"
};
return {
type: "auth-profiles.api_key.key",
path: `profiles.${profileIdTrimmed}.key`,
pathSegments: [
"profiles",
profileIdTrimmed,
"key"
],
label: `profiles.${profileIdTrimmed}.key (auth profile, agent ${agentId})`,
configFile: "auth-profile-store",
agentId,
authProfileProvider: providerTrimmed,
expectedResolvedValue: "string"
};
}
async function promptProviderAlias(params) {
const alias = assertNoCancel(await text({
message: "Provider alias",
initialValue: "default",
validate: (value) => {
const trimmed = normalizeStringifiedOptionalString(value) ?? "";
if (!trimmed) return "Required";
if (!isValidSecretProviderAlias(trimmed)) return "Must match /^[a-z][a-z0-9_-]{0,63}$/";
if (params.existingAliases.has(trimmed)) return "Alias already exists";
}
}), "Secrets configure cancelled.");
return normalizeStringifiedOptionalString(alias) ?? "";
}
async function promptProviderSource(initial) {
return assertNoCancel(await select({
message: "Provider source",
options: [
{
value: "env",
label: "env"
},
{
value: "file",
label: "file"
},
{
value: "exec",
label: "exec"
},
{
value: "store",
label: "store"
}
],
initialValue: initial
}), "Secrets configure cancelled.");
}
async function promptEnvProvider(base) {
const allowlist = await promptEnvNameCsv({
message: "Env allowlist (comma-separated, blank for unrestricted)",
initialValue: base?.allowlist?.join(",") ?? ""
});
return {
source: "env",
...allowlist.length > 0 ? { allowlist } : {}
};
}
async function promptFileProvider(base) {
const filePath = assertNoCancel(await text({
message: "File path (absolute)",
initialValue: base?.path ?? "",
validate: (value) => {
const trimmed = normalizeStringifiedOptionalString(value) ?? "";
if (!trimmed) return "Required";
if (!isAbsolutePathValue(trimmed)) return "Must be an absolute path";
}
}), "Secrets configure cancelled.");
const mode = assertNoCancel(await select({
message: "File mode",
options: [{
value: "json",
label: "json"
}, {
value: "singleValue",
label: "singleValue"
}],
initialValue: base?.mode ?? "json"
}), "Secrets configure cancelled.");
const timeoutMs = await promptOptionalPositiveInt({
message: "Timeout ms (blank for default)",
initialValue: base?.timeoutMs,
max: 12e4
});
const maxBytes = await promptOptionalPositiveInt({
message: "Max bytes (blank for default)",
initialValue: base?.maxBytes,
max: 20971520
});
return {
source: "file",
path: normalizeStringifiedOptionalString(filePath) ?? "",
mode,
...timeoutMs ? { timeoutMs } : {},
...maxBytes ? { maxBytes } : {}
};
}
async function parseArgsInput(rawValue) {
const trimmed = rawValue.trim();
if (!trimmed) return;
const parsed = JSON.parse(trimmed);
if (!Array.isArray(parsed) || !parsed.every((entry) => typeof entry === "string")) throw new Error("args must be a JSON array of strings");
return parsed;
}
async function promptExecProvider(base) {
const command = assertNoCancel(await text({
message: "Command path (absolute)",
initialValue: base?.command ?? "",
validate: (value) => {
const trimmed = normalizeStringifiedOptionalString(value) ?? "";
if (!trimmed) return "Required";
if (!isAbsolutePathValue(trimmed)) return "Must be an absolute path";
if (!isSafeExecutableValue(trimmed)) return "Command value is not allowed";
}
}), "Secrets configure cancelled.");
const argsRaw = assertNoCancel(await text({
message: "Args JSON array (blank for none)",
initialValue: JSON.stringify(base?.args ?? []),
validate: (value) => {
const trimmed = normalizeStringifiedOptionalString(value) ?? "";
if (!trimmed) return;
try {
const parsed = JSON.parse(trimmed);
if (!Array.isArray(parsed) || !parsed.every((entry) => typeof entry === "string")) return "Must be a JSON array of strings";
return;
} catch {
return "Must be valid JSON";
}
}
}), "Secrets configure cancelled.");
const timeoutMs = await promptOptionalPositiveInt({
message: "Timeout ms (blank for default)",
initialValue: base?.timeoutMs,
max: 12e4
});
const noOutputTimeoutMs = await promptOptionalPositiveInt({
message: "No-output timeout ms (blank for default)",
initialValue: base?.noOutputTimeoutMs,
max: 12e4
});
const maxOutputBytes = await promptOptionalPositiveInt({
message: "Max output bytes (blank for default)",
initialValue: base?.maxOutputBytes,
max: 20971520
});
const jsonOnly = assertNoCancel(await confirm({
message: "Require JSON-only response?",
initialValue: base?.jsonOnly ?? true
}), "Secrets configure cancelled.");
const passEnv = await promptEnvNameCsv({
message: "Pass-through env vars (comma-separated, blank for none)",
initialValue: base?.passEnv?.join(",") ?? ""
});
const trustedDirsRaw = assertNoCancel(await text({
message: "Trusted dirs (comma-separated absolute paths, blank for none)",
initialValue: base?.trustedDirs?.join(",") ?? "",
validate: (value) => {
const entries = normalizeCsvOrLooseStringList(value ?? "");
for (const entry of entries) if (!isAbsolutePathValue(entry)) return `Trusted dir must be absolute: ${entry}`;
}
}), "Secrets configure cancelled.");
const args = await parseArgsInput(normalizeStringifiedOptionalString(argsRaw) ?? "");
const trustedDirs = normalizeCsvOrLooseStringList(trustedDirsRaw ?? "");
return {
source: "exec",
command: normalizeStringifiedOptionalString(command) ?? "",
...args && args.length > 0 ? { args } : {},
...timeoutMs ? { timeoutMs } : {},
...noOutputTimeoutMs ? { noOutputTimeoutMs } : {},
...maxOutputBytes ? { maxOutputBytes } : {},
...jsonOnly ? { jsonOnly } : { jsonOnly: false },
...passEnv.length > 0 ? { passEnv } : {},
...trustedDirs.length > 0 ? { trustedDirs } : {},
...isRecord(base?.env) ? { env: base.env } : {}
};
}
async function promptProviderConfig(source, current) {
if (source === "env") return await promptEnvProvider(current?.source === "env" ? current : void 0);
if (source === "file") return await promptFileProvider(current?.source === "file" ? current : void 0);
if (source === "store") return { source: "store" };
return await promptExecProvider(current?.source === "exec" && "command" in current ? current : void 0);
}
async function configureProvidersInteractive(config, env) {
const presets = loadSecretProviderIntegrationPresets({
config,
env
});
while (true) {
const providers = getSecretProviders(config);
const providerEntries = Object.entries(providers).toSorted(([left], [right]) => left.localeCompare(right));
const presetEntries = presets.filter((preset) => {
const current = providers[preset.providerAlias];
return !current || !isDeepStrictEqual(current, preset.providerConfig);
});
const actionOptions = [{
value: "add",
label: "Add provider",
hint: "Define a new env/file/exec/store provider"
}];
if (presetEntries.length > 0) actionOptions.push({
value: "preset",
label: "Use plugin preset",
hint: "Configure a provider declared by an installed plugin"
});
if (providerEntries.length > 0) {
actionOptions.push({
value: "edit",
label: "Edit provider",
hint: "Update an existing provider"
});
actionOptions.push({
value: "remove",
label: "Remove provider",
hint: "Delete a provider alias"
});
}
actionOptions.push({
value: "continue",
label: "Continue",
hint: "Move to credential mapping"
});
const action = assertNoCancel(await select({
message: providerEntries.length > 0 ? "Configure secret providers" : "Configure secret providers (env/store refs are built in; add file/exec providers as needed)",
options: actionOptions
}), "Secrets configure cancelled.");
if (action === "continue") return;
if (action === "add") {
const source = await promptProviderSource();
setSecretProvider(config, await promptProviderAlias({ existingAliases: new Set(providerEntries.map(([providerAlias]) => providerAlias)) }), await promptProviderConfig(source));
continue;
}
if (action === "preset") {
const selectedPresetKey = assertNoCancel(await select({
message: "Select plugin preset",
options: presetEntries.map((preset) => ({
value: providerPresetKey(preset),
label: preset.displayName,
hint: providerPresetHint(preset)
}))
}), "Secrets configure cancelled.");
const preset = presetEntries.find((entry) => providerPresetKey(entry) === selectedPresetKey);
if (!preset) throw new Error(`Unknown secret provider preset: ${selectedPresetKey}`);
if (providers[preset.providerAlias]) {
if (!assertNoCancel(await confirm({
message: `Replace provider "${preset.providerAlias}" with the ${preset.displayName} preset?`,
initialValue: false
}), "Secrets configure cancelled.")) continue;
}
setSecretProvider(config, preset.providerAlias, structuredClone(preset.providerConfig));
continue;
}
if (action === "edit") {
const alias = assertNoCancel(await select({
message: "Select provider to edit",
options: providerEntries.map(([providerAlias, providerConfig]) => ({
value: providerAlias,
label: providerAlias,
hint: providerHint(providerConfig)
}))
}), "Secrets configure cancelled.");
const current = providers[alias];
if (!current) continue;
const nextProviderConfig = await promptProviderConfig(await promptProviderSource(current.source), current);
if (!isDeepStrictEqual(current, nextProviderConfig)) setSecretProvider(config, alias, nextProviderConfig);
continue;
}
if (action === "remove") {
const alias = assertNoCancel(await select({
message: "Select provider to remove",
options: providerEntries.map(([providerAlias, providerConfig]) => ({
value: providerAlias,
label: providerAlias,
hint: providerHint(providerConfig)
}))
}), "Secrets configure cancelled.");
if (assertNoCancel(await confirm({
message: `Remove provider "${alias}"?`,
initialValue: false
}), "Secrets configure cancelled.")) removeSecretProvider(config, alias);
}
}
}
/** Runs interactive secrets configuration and returns changed config/auth-store state. */
async function runSecretsConfigureInteractive(params = {}) {
if (!process.stdin.isTTY) throw new Error("secrets configure requires an interactive TTY.");
if (params.providersOnly && params.skipProviderSetup) throw new Error("Cannot combine --providers-only with --skip-provider-setup.");
const env = params.env ?? process.env;
const allowExecInPreflight = Boolean(params.allowExecInPreflight);
const { snapshot } = await createSecretsConfigIO({ env }).readConfigFileSnapshotForWrite();
if (!snapshot.valid) throw new Error("Cannot run interactive secrets configure because config is invalid.");
const stagedConfig = structuredClone(snapshot.config);
if (!params.skipProviderSetup) await configureProvidersInteractive(stagedConfig, env);
const providerChanges = collectConfigureProviderChanges({
original: snapshot.config,
next: stagedConfig
});
const selectedByPath = /* @__PURE__ */ new Map();
if (!params.providersOnly) {
const configureAgentId = resolveConfigureAgentId(snapshot.config, params.agentId);
const authStore = loadAuthProfileStoreForConfigure({
config: snapshot.config,
agentId: configureAgentId
});
const candidates = buildConfigureCandidatesForScope({
config: stagedConfig,
authoredOpenClawConfig: snapshot.resolved,
authProfiles: {
agentId: configureAgentId,
store: authStore
}
});
if (candidates.length === 0) throw new Error("No configurable secret-bearing fields found for this agent scope.");
const sourceChoices = toSourceChoices(stagedConfig);
const hasDerivedCandidates = candidates.some((candidate) => candidate.isDerived === true);
let showDerivedCandidates = false;
while (true) {
const visibleCandidates = showDerivedCandidates ? candidates : candidates.filter((candidate) => candidate.isDerived !== true);
const options = visibleCandidates.map((candidate) => ({
value: configureCandidateKey(candidate),
label: candidate.label,
hint: [candidate.configFile === "auth-profile-store" ? "auth profile store" : "openclaw.json", candidate.isDerived === true ? "derived" : void 0].filter(Boolean).join(" | ")
}));
options.push({
value: "__create_auth_profile__",
label: "Create auth profile mapping",
hint: `Add a new auth-profiles target for agent ${configureAgentId}`
});
if (hasDerivedCandidates) options.push({
value: "__toggle_derived__",
label: showDerivedCandidates ? "Hide derived targets" : "Show derived targets",
hint: showDerivedCandidates ? "Show only fields authored directly in config" : "Include normalized/derived aliases"
});
if (selectedByPath.size > 0) options.unshift({
value: "__done__",
label: "Done",
hint: "Finish and run preflight"
});
const selectedPath = assertNoCancel(await select({
message: "Select credential field",
options
}), "Secrets configure cancelled.");
if (selectedPath === "__done__") break;
if (selectedPath === "__create_auth_profile__") {
const createdCandidate = await promptNewAuthProfileCandidate(configureAgentId);
const key = configureCandidateKey(createdCandidate);
const existingIndex = candidates.findIndex((entry) => configureCandidateKey(entry) === key);
if (existingIndex >= 0) candidates[existingIndex] = createdCandidate;
else candidates.push(createdCandidate);
continue;
}
if (selectedPath === "__toggle_derived__") {
showDerivedCandidates = !showDerivedCandidates;
continue;
}
const candidate = visibleCandidates.find((entry) => configureCandidateKey(entry) === selectedPath);
if (!candidate) throw new Error(`Unknown configure target: ${selectedPath}`);
const candidateKey = configureCandidateKey(candidate);
const existingRef = selectedByPath.get(candidateKey)?.ref ?? candidate.existingRef;
const sourceInitialValue = existingRef && hasSourceChoice(sourceChoices, existingRef.source) ? existingRef.source : void 0;
const source = assertNoCancel(await select({
message: "Secret source",
options: sourceChoices,
initialValue: sourceInitialValue
}), "Secrets configure cancelled.");
const defaultAlias = resolveDefaultSecretProviderAlias(stagedConfig, source, { preferFirstProviderForSource: true });
const providerInitialValue = existingRef?.source === source ? existingRef.provider : defaultAlias;
const provider = assertNoCancel(await text({
message: "Provider alias",
initialValue: providerInitialValue,
validate: (value) => {
const trimmed = normalizeStringifiedOptionalString(value) ?? "";
if (!trimmed) return "Required";
if (!isValidSecretProviderAlias(trimmed)) return "Must match /^[a-z][a-z0-9_-]{0,63}$/";
}
}), "Secrets configure cancelled.");
const providerAlias = normalizeStringifiedOptionalString(provider) ?? "";
let suggestedId = existingRef?.source === source ? existingRef.id : void 0;
if (!suggestedId && (source === "env" || source === "store")) suggestedId = resolveSuggestedEnvSecretId(candidate);
if (!suggestedId && source === "file") {
const configuredProvider = stagedConfig.secrets?.providers?.[providerAlias];
if (configuredProvider?.source === "file" && configuredProvider.mode === "singleValue") suggestedId = "value";
}
const id = assertNoCancel(await text({
message: "Secret id",
initialValue: suggestedId,
validate: (value) => {
const trimmed = normalizeStringifiedOptionalString(value) ?? "";
if (!trimmed) return "Required";
if ((source === "env" || source === "store") && !isValidEnvSecretRefId(trimmed)) return `${source} ids must match /^[A-Z][A-Z0-9_]{0,127}$/`;
if (source === "exec" && !isValidExecSecretRefId(trimmed)) return formatExecSecretRefIdValidationMessage();
}
}), "Secrets configure cancelled.");
const ref = {
source,
provider: providerAlias,
id: normalizeStringifiedOptionalString(id) ?? ""
};
if (ref.source === "exec" && !allowExecInPreflight) {
const staticError = getSkippedExecRefStaticError({
ref,
config: stagedConfig
});
if (staticError) throw new Error(staticError);
} else {
const resolved = await resolveSecretRefValue(ref, {
config: stagedConfig,
env
});
assertExpectedResolvedSecretValue({
value: resolved,
expected: candidate.expectedResolvedValue,
errorMessage: candidate.expectedResolvedValue === "string" ? `Ref ${ref.source}:${ref.provider}:${ref.id} did not resolve to a non-empty string.` : `Ref ${ref.source}:${ref.provider}:${ref.id} did not resolve to a supported value type.`
});
}
const next = {
...candidate,
ref
};
selectedByPath.set(candidateKey, next);
if (!assertNoCancel(await confirm({
message: "Configure another credential?",
initialValue: true
}), "Secrets configure cancelled.")) break;
}
}
if (!hasConfigurePlanChanges({
selectedTargets: selectedByPath,
providerChanges
})) throw new Error("No secrets changes were selected.");
const plan = buildSecretsConfigurePlan({
selectedTargets: selectedByPath,
providerChanges
});
return {
plan,
preflight: await runSecretsApply({
plan,
env,
write: false,
allowExec: allowExecInPreflight
})
};
}
//#endregion
export { runSecretsConfigureInteractive };