openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
1,909 lines • 89.5 kB
JavaScript
import { w as parseStrictPositiveInteger } from "./number-coercion-CLj0HTDM.js";
import { nt as isConfigSetJsonParseOnly, tt as isConfigMachineOutput } from "./argv-DNjh_yaF.js";
import "./src-vebZIeLe.js";
import { t as expectDefined } from "./expect-CyE8FADM.js";
import { c as isRecord } from "./record-coerce-DItp3I4t.js";
import { l as normalizeOptionalString, p as normalizeStringifiedOptionalString } from "./string-coerce-CIXf7egm.js";
import { b as uniqueValues, d as normalizeStringEntries } from "./string-normalization-DsCfAx8q.js";
import { a as writeRuntimeJson, o as writeRuntimeStdout, r as defaultRuntime, t as ExitError } from "./runtime-CF2WjnNZ.js";
import { t as exitCliAfterOutput } from "./one-shot-exit-BkFPnjbM.js";
import { l as readConfigFileSnapshotForWrite, u as readConfigFileSnapshotWithPluginMetadata } from "./io.runtime-B9iJRs3w.js";
import { l as readFileDescriptorBoundedSync } from "./boundary-file-read-uaJcf6X6.js";
import { t as hasErrnoCode } from "./errno-CkbDOfLk.js";
import { S as resolveConfigEnvVars, x as containsEnvVarReference } from "./redact-BtvPPfTi.js";
import { m as shortenHomePath } from "./utils-P__uGsPB.js";
import { n as normalizeAgentId } from "./agent-id-CeT3w4ap.js";
import { t as formatCliCommand } from "./command-format-C7YfyMTd.js";
import { D as tryResolveLegacyCompatibilityAgentId, a as listAgentEntriesWithSource, i as listAgentEntries, n as hasAgentRosterProperty, s as readAgentRosterProperty } from "./agent-scope-config-DcbEhP0R.js";
import { f as resolveConfigPath, t as CONFIG_PATH } from "./paths-D2sRr1a_.js";
import "./session-key-BnWWjqNc.js";
import { a as parseConcreteConfigPathWithProvenance, i as parseConcreteConfigPathTokens, o as toDotPath, r as parseConcreteConfigPath } from "./dot-path-BOSboevO.js";
import { d as isSecretRef, p as isValidEnvSecretRefId, s as coerceSecretRef, v as resolveSecretInputRef } from "./types.secrets-kC0nOetj.js";
import { t as formatErrorMessage } from "./errors-Db3Ymjlb.js";
import { c as isValidSecretProviderAlias, f as secretRefKey, i as formatExecSecretRefIdValidationMessage, o as isValidExecSecretRefId, p as validateExecSecretRefId, s as isValidFileSecretRefId } from "./ref-contract-D92DqQ-r.js";
import { T as SecretProviderSchema } from "./zod-schema.core-D6k6NKKA.js";
import { a as loadPluginMetadataSnapshot } from "./plugin-metadata-snapshot-w-4EjxI4.js";
import { n as parseLegacyAgentRoster, t as migratePersistedImplicitMainRoster } from "./legacy.roster-CQ9vDbV2.js";
import { g as resolveConfiguredModelRef, i as buildModelAliasIndex, y as resolveModelRefFromString } from "./model-selection-shared-BlLyx1r2.js";
import { r as DEFAULT_PROVIDER } from "./defaults-CdX9UGcX.js";
import { t as splitTrailingAuthProfile } from "./model-ref-profile-BIKs-96s.js";
import { a as resolveAgentModelPrimaryValue, n as normalizeAgentModelRefForConfig } from "./model-input-BuGMCNOz.js";
import { c as resolveAgentExplicitModelPrimary, u as resolveAgentModelFallbacksOverride } from "./agent-scope-DbtJyKUL.js";
import { t as resolveDefaultModelForAgent } from "./model-selection-config-BrdmmqKD.js";
import { W as AUTO_MANAGED_CONFIG_META_PATHS, c as resolveManagedUnsetPathsForWrite, m as validateConfigObjectRawWithPlugins, v as collectUnsupportedSecretRefPolicyIssues } from "./io.types-BUCjdS5v.js";
import { u as rejectConfigNonFiniteNumbers, x as visitConfigValueTree } from "./io.read-helpers-ZKp-UiGx.js";
import { t as ConfigMutationConflictError } from "./mutation-conflict-Be0wSyDG.js";
import { i as normalizeConfigIssues, n as formatConfigIssueLines } from "./issue-format-CQvxWNW8.js";
import { c as resolveConfigSecretTargetByPath, n as discoverConfigSecretTargets } from "./target-registry-query-qVhyD-Hq.js";
import "./target-registry-BHDUOLUU.js";
import { n as isPluginPackagingRuntimeOutputInvalidConfigSnapshot } from "./recovery-policy-CsUZ07YX.js";
import { r as replaceConfigFile } from "./mutate-ZNN4iFCn.js";
import "./config-Cs0XXL3x.js";
import { t as formatDocsLink } from "./links-ClIwBcy4.js";
import { r as theme } from "./theme-vjDs9tao.js";
import { a as assertSecureExecCommandPath, c as resolveSecretProviderIntegrationConfig, n as resolveSecretRefValue, o as isPluginIntegrationSecretProviderConfig } from "./resolve-224YoYfx.js";
import { a as success, n as info, o as warn, t as danger } from "./globals-CTaGxEqj.js";
import { n as formatPluginPackagingRuntimeOutputRecoveryHint } from "./config-recovery-hints-BS5G0aQ-.js";
import { t as renderConfigValidationIssueLines } from "./issue-location-Dbpi1Rol.js";
import { r as formatCliJsonFailure } from "./failure-output-Dc5ce9_F.js";
import { t as quoteCliArg } from "./quote-cli-arg-BEt71TUh.js";
import { n as redactConfigObject } from "./redact-snapshot-DcfZQ78H.js";
import { t as readByteStreamWithLimit } from "./read-byte-stream-with-limit-CNew-qG0.js";
import { a as isConfigSchemaPath, c as setAtPath, i as getAtPath, l as unsetAtPath, n as formatConfigSetPath, o as mergeAtPath, r as formatConfigUnsetMissingPathMessage, s as parseConfigSetValue, t as assertNonDestructiveReplacement, u as validatePathSegments } from "./config-cli-path-BHk6DaYo.js";
import { r as readBestEffortRuntimeConfigSchema, t as buildRuntimeConfigSchemaFromRegistry } from "./runtime-schema-D3-BdLtQ.js";
import { t as prepareConfigWriteTopology } from "./io.write-topology-BvvklbCP.js";
import { t as diffConfigPaths } from "./config-diff-D4CS9rJs.js";
import { t as buildGatewayReloadPlan } from "./config-reload-plan-DUT3qlnP.js";
import { t as resolveGatewayReloadSettings } from "./config-reload-settings-q1wYjpRM.js";
import { t as normalizeSubmittedConfigModelRefs } from "./model-input-normalization-D35tM3yN.js";
import { n as setCommandJsonMode } from "./json-mode-jHy4Wzfh.js";
import fs from "node:fs";
import { isDeepStrictEqual } from "node:util";
import JSON5 from "json5";
//#region src/cli/config-set-input.ts
const CONFIG_MUTATION_FILE_MAX_BYTES = 8388608;
function readConfigMutationFileSync(filePath, sourceLabel) {
let fd;
try {
fd = fs.openSync(filePath, "r");
} catch (error) {
if (hasErrnoCode(error, "ENOENT")) throw new Error(`${sourceLabel} not found: ${filePath}. Check the path and try again.`, { cause: error });
throw error;
}
try {
if (!fs.fstatSync(fd).isFile()) throw new Error(`${sourceLabel} must be a regular file: ${filePath}. Choose a JSON5 input file and try again.`);
try {
return readFileDescriptorBoundedSync(fd, CONFIG_MUTATION_FILE_MAX_BYTES).toString("utf8");
} catch (error) {
if (error instanceof RangeError) throw new RangeError(`${sourceLabel} exceeds the 8 MiB supported maximum (${CONFIG_MUTATION_FILE_MAX_BYTES} bytes): ${filePath}`, { cause: error });
throw error;
}
} finally {
fs.closeSync(fd);
}
}
function hasBatchMode(opts) {
return Boolean(normalizeOptionalString(opts.batchJson) || normalizeOptionalString(opts.batchFile));
}
function hasRefBuilderOptions(opts) {
return Boolean(opts.refProvider || opts.refSource || opts.refId);
}
function hasProviderBuilderOptions(opts) {
return Boolean(opts.providerSource || opts.providerAllowlist?.length || opts.providerPath || opts.providerMode || opts.providerTimeoutMs || opts.providerMaxBytes || opts.providerCommand || opts.providerArg?.length || opts.providerNoOutputTimeoutMs || opts.providerMaxOutputBytes || opts.providerJsonOnly || opts.providerEnv?.length || opts.providerPassEnv?.length || opts.providerTrustedDir?.length);
}
function parseJson5Raw(raw, label) {
let parsed;
try {
parsed = JSON5.parse(raw);
} catch (err) {
throw new Error(`Failed to parse ${label}: ${String(err)}`, { cause: err });
}
rejectConfigNonFiniteNumbers(parsed);
return parsed;
}
function parseBatchEntries(raw, sourceLabel) {
const parsed = parseJson5Raw(raw, sourceLabel);
if (!Array.isArray(parsed)) throw new Error(`${sourceLabel} must be a JSON array.`);
if (parsed.length === 0) throw new Error(`${sourceLabel} must contain at least one config update.`);
const out = [];
for (const [index, entry] of parsed.entries()) {
if (!entry || typeof entry !== "object" || Array.isArray(entry)) throw new Error(`${sourceLabel}[${index}] must be an object.`);
const typed = entry;
const path = normalizeOptionalString(typed.path) ?? "";
if (!path) throw new Error(`${sourceLabel}[${index}].path is required.`);
const hasValue = Object.hasOwn(typed, "value");
const hasRef = Object.hasOwn(typed, "ref");
const hasProvider = Object.hasOwn(typed, "provider");
if (Number(hasValue) + Number(hasRef) + Number(hasProvider) !== 1) throw new Error(`${sourceLabel}[${index}] must include exactly one of: value, ref, provider.`);
out.push({
path,
...hasValue ? { value: typed.value } : {},
...hasRef ? { ref: typed.ref } : {},
...hasProvider ? { provider: typed.provider } : {}
});
}
return out;
}
function parseConfigSetCurrentExpectation(opts) {
const expectAbsent = opts.expectCurrentAbsent === true;
const hasExpectedJson = opts.expectCurrentJson !== void 0;
if (!expectAbsent && !hasExpectedJson) return;
if (expectAbsent && hasExpectedJson) throw new Error("config set mode error: choose either --expect-current-absent or --expect-current-json, not both.");
if (opts.dryRun) throw new Error("config set mode error: conditional expectations cannot be combined with --dry-run.");
if (opts.batchJson !== void 0 || opts.batchFile !== void 0) throw new Error("config set mode error: conditional expectations require one path operation and cannot be combined with batch mode.");
if (expectAbsent) return { kind: "absent" };
const expectedJson = opts.expectCurrentJson;
if (expectedJson === void 0) throw new Error("config set mode error: missing conditional expectation.");
let value;
try {
value = JSON.parse(expectedJson);
rejectConfigNonFiniteNumbers(value);
} catch (error) {
throw new Error("config set mode error: --expect-current-json must be valid JSON.", { cause: error });
}
return {
kind: "json",
value
};
}
function parseBatchSource(opts) {
const batchJson = normalizeOptionalString(opts.batchJson);
const batchFile = normalizeOptionalString(opts.batchFile);
const hasInline = Boolean(batchJson);
const hasFile = Boolean(batchFile);
if (!hasInline && !hasFile) return null;
if (hasInline && hasFile) throw new Error("Use either --batch-json or --batch-file, not both.");
if (hasInline) return parseBatchEntries(batchJson, "--batch-json");
const pathname = normalizeStringifiedOptionalString(opts.batchFile) ?? "";
if (!pathname) throw new Error("--batch-file must not be empty.");
return parseBatchEntries(readConfigMutationFileSync(pathname, "--batch-file"), "--batch-file");
}
//#endregion
//#region src/cli/config-set-parser.ts
/** Resolve the config-set input mode or return the exact flag-conflict error. */
function resolveConfigSetMode(params) {
if (params.hasBatchMode) {
if (params.hasRefBuilderOptions || params.hasProviderBuilderOptions) return {
ok: false,
error: "batch mode (--batch-json/--batch-file) cannot be combined with ref builder (--ref-*) or provider builder (--provider-*) flags."
};
return {
ok: true,
mode: "batch"
};
}
if (params.hasRefBuilderOptions && params.hasProviderBuilderOptions) return {
ok: false,
error: "choose exactly one mode: ref builder (--ref-provider/--ref-source/--ref-id) or provider builder (--provider-*), not both."
};
if (params.hasRefBuilderOptions) return {
ok: true,
mode: "ref_builder"
};
if (params.hasProviderBuilderOptions) return {
ok: true,
mode: "provider_builder"
};
return {
ok: true,
mode: params.strictJson ? "json" : "value"
};
}
//#endregion
//#region src/cli/config-cli-input.ts
const CONFIG_PATCH_STDIN_MAX_BYTES = 1048576;
function modeError(message) {
return /* @__PURE__ */ new Error(`config set mode error: ${message}`);
}
function configPatchModeError(message) {
return /* @__PURE__ */ new Error(`config patch mode error: ${message}`);
}
function parseSecretRefSource(raw, label) {
const source = raw.trim();
if (source === "env" || source === "file" || source === "exec" || source === "store") return source;
throw new Error(`${label} must be one of: env, file, exec, store.`);
}
function parseSecretRefBuilder(params) {
const provider = params.provider.trim();
if (!provider) throw new Error(`${params.fieldPrefix}.provider is required.`);
if (!isValidSecretProviderAlias(provider)) throw new Error(`${params.fieldPrefix}.provider must match /^[a-z][a-z0-9_-]{0,63}$/ (example: "default").`);
const source = parseSecretRefSource(params.source, `${params.fieldPrefix}.source`);
const id = params.id.trim();
if (!id) throw new Error(`${params.fieldPrefix}.id is required.`);
if (source === "env" && !isValidEnvSecretRefId(id)) throw new Error(`${params.fieldPrefix}.id must match /^[A-Z][A-Z0-9_]{0,127}$/ for env refs.`);
if (source === "store" && !isValidEnvSecretRefId(id)) throw new Error(`${params.fieldPrefix}.id must match /^[A-Z][A-Z0-9_]{0,127}$/ for store refs.`);
if (source === "file" && !isValidFileSecretRefId(id)) throw new Error(`${params.fieldPrefix}.id must be an absolute JSON pointer (or "value" for singleValue mode).`);
if (source === "exec" && !validateExecSecretRefId(id).ok) throw new Error(formatExecSecretRefIdValidationMessage());
return {
source,
provider,
id
};
}
function parseOptionalPositiveInteger(raw, flag) {
if (raw === void 0) return;
const trimmed = raw.trim();
if (!trimmed) throw new Error(`${flag} must not be empty.`);
const parsed = parseStrictPositiveInteger(trimmed);
if (parsed === void 0) throw new Error(`${flag} must be a positive integer.`);
return parsed;
}
function parseProviderEnvEntries(entries) {
if (!entries || entries.length === 0) return;
const env = {};
for (const entry of entries) {
const separator = entry.indexOf("=");
if (separator <= 0) throw new Error("--provider-env expects KEY=*** entries.");
const key = entry.slice(0, separator).trim();
if (!key) throw new Error("--provider-env key must not be empty.");
env[key] = entry.slice(separator + 1);
}
return Object.keys(env).length > 0 ? env : void 0;
}
function validateProviderAliasPath(path) {
if (path.length !== 3 || path[0] !== "secrets" || path[1] !== "providers") throw new Error("Provider builder mode requires path \"secrets.providers.<alias>\" (example: secrets.providers.vault).");
const alias = path[2] ?? "";
if (!isValidSecretProviderAlias(alias)) throw new Error(`Provider alias "${alias}" must match /^[a-z][a-z0-9_-]{0,63}$/ (example: "default").`);
}
function buildProviderFromBuilder(opts) {
const sourceRaw = opts.providerSource?.trim();
if (!sourceRaw) throw new Error("--provider-source is required in provider builder mode.");
const source = parseSecretRefSource(sourceRaw, "--provider-source");
const timeoutMs = parseOptionalPositiveInteger(opts.providerTimeoutMs, "--provider-timeout-ms");
const maxBytes = parseOptionalPositiveInteger(opts.providerMaxBytes, "--provider-max-bytes");
const noOutputTimeoutMs = parseOptionalPositiveInteger(opts.providerNoOutputTimeoutMs, "--provider-no-output-timeout-ms");
const maxOutputBytes = parseOptionalPositiveInteger(opts.providerMaxOutputBytes, "--provider-max-output-bytes");
const providerEnv = parseProviderEnvEntries(opts.providerEnv);
let provider;
if (source === "env") {
const allowlist = normalizeStringEntries(opts.providerAllowlist);
for (const envName of allowlist) if (!isValidEnvSecretRefId(envName)) throw new Error(`--provider-allowlist entry "${envName}" must match /^[A-Z][A-Z0-9_]{0,127}$/.`);
provider = {
source: "env",
...allowlist.length > 0 ? { allowlist } : {}
};
} else if (source === "file") {
const filePath = opts.providerPath?.trim();
if (!filePath) throw new Error("--provider-path is required when --provider-source file is used.");
const modeRaw = opts.providerMode?.trim();
if (modeRaw && modeRaw !== "singleValue" && modeRaw !== "json") throw new Error("--provider-mode must be one of: singleValue, json.");
const mode = modeRaw === "singleValue" || modeRaw === "json" ? modeRaw : void 0;
provider = {
source: "file",
path: filePath,
...mode ? { mode } : {},
...timeoutMs !== void 0 ? { timeoutMs } : {},
...maxBytes !== void 0 ? { maxBytes } : {}
};
} else if (source === "store") provider = { source: "store" };
else {
const command = opts.providerCommand?.trim();
if (!command) throw new Error("--provider-command is required when --provider-source exec is used.");
provider = {
source: "exec",
command,
...opts.providerArg?.length ? { args: opts.providerArg.map((entry) => entry.trim()) } : {},
...timeoutMs !== void 0 ? { timeoutMs } : {},
...noOutputTimeoutMs !== void 0 ? { noOutputTimeoutMs } : {},
...maxOutputBytes !== void 0 ? { maxOutputBytes } : {},
...opts.providerJsonOnly ? { jsonOnly: true } : {},
...providerEnv ? { env: providerEnv } : {},
...opts.providerPassEnv?.length ? { passEnv: normalizeStringEntries(opts.providerPassEnv) } : {},
...opts.providerTrustedDir?.length ? { trustedDirs: normalizeStringEntries(opts.providerTrustedDir) } : {}
};
}
const validated = SecretProviderSchema.safeParse(provider);
if (!validated.success) {
const issue = validated.error.issues[0];
throw new Error(`Provider builder config invalid at ${issue?.path?.join(".") ?? "<provider>"}: ${issue?.message ?? "Invalid provider config."}`);
}
return validated.data;
}
function parseSecretRefFromUnknown(value, label) {
if (!isRecord(value)) throw new Error(`${label} must be an object with source/provider/id.`);
if (typeof value.provider !== "string" || typeof value.source !== "string" || typeof value.id !== "string") throw new Error(`${label} must include string fields: source, provider, id.`);
return parseSecretRefBuilder({
provider: value.provider,
source: value.source,
id: value.id,
fieldPrefix: label
});
}
function buildAssignmentOperation(params) {
const resolved = resolveConfigSecretTargetByPath(params.requestedPath, params.pathTokens);
const coercedRef = coerceSecretRef(params.value);
return {
inputMode: params.inputMode,
requestedPath: params.requestedPath,
...params.pathTokens ? { pathTokens: params.pathTokens } : {},
...params.quotedNumericSegments ? { quotedNumericSegments: params.quotedNumericSegments } : {},
setPath: coercedRef && resolved?.entry.secretShape === "sibling_ref" && resolved.refPathSegments ? resolved.refPathSegments : params.requestedPath,
value: params.value,
...params.validatedRef && resolved ? { schemaValidated: true } : {}
};
}
function parseBatchOperations(entries) {
return entries.map((entry, index) => {
const { tokens: pathTokens, quotedNumericSegments } = parseConcreteConfigPathWithProvenance(entry.path);
const path = pathTokens.map(String);
if (entry.ref !== void 0) return buildAssignmentOperation({
requestedPath: path,
pathTokens,
quotedNumericSegments,
value: parseSecretRefFromUnknown(entry.ref, `batch[${index}].ref`),
inputMode: "json",
validatedRef: true
});
if (entry.provider !== void 0) {
validateProviderAliasPath(path);
const validated = SecretProviderSchema.safeParse(entry.provider);
if (!validated.success) {
const issue = validated.error.issues[0];
throw new Error(`batch[${index}].provider invalid at ${issue?.path?.join(".") ?? "<provider>"}: ${issue?.message ?? ""}`);
}
return {
inputMode: "json",
requestedPath: path,
pathTokens,
quotedNumericSegments,
setPath: path,
value: validated.data,
schemaValidated: true
};
}
return buildAssignmentOperation({
requestedPath: path,
pathTokens,
quotedNumericSegments,
value: entry.value,
inputMode: "json"
});
});
}
function buildConfigSetOperations(params) {
const strictJson = Boolean(params.opts.strictJson || params.opts.json);
const modeResolution = resolveConfigSetMode({
hasBatchMode: hasBatchMode(params.opts),
hasRefBuilderOptions: hasRefBuilderOptions(params.opts),
hasProviderBuilderOptions: hasProviderBuilderOptions(params.opts),
strictJson
});
if (!modeResolution.ok) throw modeError(modeResolution.error);
if (params.opts.allowExec && !params.opts.dryRun) throw modeError("--allow-exec requires --dry-run.");
if (params.opts.merge && params.opts.replace) throw modeError("choose either --merge or --replace, not both.");
const batchEntries = parseBatchSource(params.opts);
if (batchEntries) {
if (params.path !== void 0 || params.value !== void 0) throw modeError("batch mode does not accept <path> or <value> arguments.");
return parseBatchOperations(batchEntries);
}
const pathProvided = typeof params.path === "string" && params.path.trim().length > 0;
const parsedConcretePath = pathProvided ? parseConcreteConfigPathWithProvenance(params.path) : null;
const pathTokens = parsedConcretePath?.tokens ?? null;
const parsedPath = pathTokens?.map(String) ?? null;
if (modeResolution.mode === "ref_builder") {
if (!pathProvided || !parsedPath) throw modeError("ref builder mode requires <path>.");
if (params.value !== void 0) throw modeError("ref builder mode does not accept <value>.");
if (!params.opts.refProvider || !params.opts.refSource || !params.opts.refId) throw modeError("ref builder mode requires --ref-provider <alias>, --ref-source <env|file|exec|store>, and --ref-id <id>.");
return [buildAssignmentOperation({
requestedPath: parsedPath,
pathTokens: pathTokens ?? void 0,
quotedNumericSegments: parsedConcretePath?.quotedNumericSegments,
value: parseSecretRefBuilder({
provider: params.opts.refProvider,
source: params.opts.refSource,
id: params.opts.refId,
fieldPrefix: "ref"
}),
inputMode: "builder",
validatedRef: true
})];
}
if (modeResolution.mode === "provider_builder") {
if (!pathProvided || !parsedPath) throw modeError("provider builder mode requires <path>.");
if (params.value !== void 0) throw modeError("provider builder mode does not accept <value>.");
const value = buildProviderFromBuilder(params.opts);
validateProviderAliasPath(parsedPath);
return [{
inputMode: "builder",
requestedPath: parsedPath,
...pathTokens ? { pathTokens } : {},
...parsedConcretePath ? { quotedNumericSegments: parsedConcretePath.quotedNumericSegments } : {},
setPath: parsedPath,
value,
schemaValidated: true
}];
}
if (!pathProvided || !parsedPath) throw modeError("value/json mode requires <path> when batch mode is not used.");
if (params.value === void 0) throw modeError("value/json mode requires <value>.");
return [buildAssignmentOperation({
requestedPath: parsedPath,
pathTokens: pathTokens ?? void 0,
quotedNumericSegments: parsedConcretePath?.quotedNumericSegments,
value: parseConfigSetValue(params.value, strictJson),
inputMode: modeResolution.mode === "json" ? "json" : "value"
})];
}
async function readStdinText() {
if (process.stdin.isTTY) throw configPatchModeError("--stdin refuses to read from an interactive terminal; pipe input or use --file <path>.");
process.stdin.setEncoding("utf8");
return (await readByteStreamWithLimit(process.stdin, {
maxBytes: CONFIG_PATCH_STDIN_MAX_BYTES,
onOverflow: ({ maxBytes }) => configPatchModeError(`--stdin input exceeds ${maxBytes} bytes; use --file <path> for larger patches.`)
})).toString("utf8");
}
async function readConfigPatchInput(opts) {
const file = normalizeOptionalString(opts.file);
const stdin = Boolean(opts.stdin);
if (Boolean(file) === stdin) throw configPatchModeError("provide exactly one of --file <path> or --stdin.");
const sourceLabel = stdin ? "--stdin" : "--file";
let raw;
if (stdin) raw = await readStdinText();
else raw = readConfigMutationFileSync(file, "--file");
let parsed;
try {
parsed = JSON5.parse(raw);
} catch (err) {
throw new Error(`Failed to parse ${sourceLabel} as JSON5: ${String(err)}`, { cause: err });
}
rejectConfigNonFiniteNumbers(parsed);
return parsed;
}
function buildDeleteOperation(path) {
return {
...buildUnsetOperation(path),
inputMode: "json"
};
}
function buildUnsetOperation(path, pathTokens) {
return {
inputMode: "unset",
requestedPath: path,
...pathTokens ? { pathTokens } : {},
setPath: path,
value: void 0,
mutation: "delete"
};
}
function buildApplyValueOperation(params) {
const ref = isRecord(params.value) ? coerceSecretRef(params.value) : null;
return {
...buildAssignmentOperation({
requestedPath: params.path,
value: ref ? parseSecretRefFromUnknown(params.value, `patch.${toDotPath(params.path)}`) : params.value,
inputMode: "json",
validatedRef: Boolean(ref)
}),
...params.mutation ? { mutation: params.mutation } : {}
};
}
function buildConfigPatchOperations(params) {
if (!isRecord(params.patch)) throw configPatchModeError("input must be a JSON5 object patch.");
const operations = [];
const pathKey = (path) => JSON.stringify(path);
const replacePathsByLength = /* @__PURE__ */ new Map();
for (const replacePath of params.replacePaths) {
const sameLength = replacePathsByLength.get(replacePath.length);
const candidate = {
path: replacePath,
key: pathKey(replacePath)
};
if (sameLength) sameLength.push(candidate);
else replacePathsByLength.set(replacePath.length, [candidate]);
}
const matchedReplacePathKeys = /* @__PURE__ */ new Set();
visitConfigValueTree(params.patch, (value, path) => {
const segment = path.at(-1);
if (segment !== void 0) validatePathSegments([segment]);
const replacementPath = replacePathsByLength.get(path.length)?.find((candidate) => candidate.path.every((candidateSegment, index) => candidateSegment === path[index]));
if (path.length > 0 && replacementPath) {
matchedReplacePathKeys.add(replacementPath.key);
const operationPath = [...path];
operations.push(value === null ? buildDeleteOperation(operationPath) : buildApplyValueOperation({
path: operationPath,
value,
mutation: "replace"
}));
return false;
}
if (path.length > 0 && value === null) {
operations.push(buildDeleteOperation([...path]));
return false;
}
if (path.length > 0 && isRecord(value) && coerceSecretRef(value)) {
operations.push(buildApplyValueOperation({
path: [...path],
value
}));
return false;
}
if (isRecord(value)) {
if (path.length > 0 && Object.keys(value).length === 0) {
operations.push(buildApplyValueOperation({
path: [...path],
value,
mutation: "merge"
}));
return false;
}
return true;
}
if (path.length === 0) throw configPatchModeError("input must contain at least one config key.");
operations.push(buildApplyValueOperation({
path: [...path],
value
}));
return false;
});
const unusedReplacePath = params.replacePaths.find((replacePath) => !matchedReplacePathKeys.has(pathKey(replacePath)));
if (unusedReplacePath) throw configPatchModeError(`--replace-path ${toDotPath(unusedReplacePath)} did not match any value in the input patch.`);
if (operations.length === 0) throw configPatchModeError("input patch did not contain any config updates.");
return operations;
}
async function readConfigPatchOperations(opts) {
return buildConfigPatchOperations({
patch: await readConfigPatchInput(opts),
replacePaths: (opts.replacePath ?? []).map(parseConcreteConfigPath)
});
}
function formatPluginInstallConfigSetError() {
return [
"plugins.installs is managed by the plugin index and cannot be edited with config set.",
"",
"Use plugin commands instead:",
` ${formatCliCommand("openclaw plugins install <spec>")}`,
` ${formatCliCommand("openclaw plugins update <plugin-id>")}`,
` ${formatCliCommand("openclaw plugins uninstall <plugin-id>")}`
].join("\n");
}
//#endregion
//#region src/cli/config-cli-model-normalization.ts
function normalizeConfigMutationModelRefs(cfg) {
const pluginMetadata = loadPluginMetadataSnapshot({
config: cfg,
env: process.env
});
return normalizeSubmittedConfigModelRefs(cfg, pluginMetadata.owners.modelIdNormalizationPolicies);
}
function normalizeConfigMutationExplicitSetPath(path) {
const modelKeyIndex = path[0] === "agents" && path[1] === "defaults" && path[2] === "models" ? 3 : path[0] === "agents" && (path[1] === "entries" || path[1] === "list") && path[3] === "models" ? 4 : void 0;
if (modelKeyIndex !== void 0 && path.length > modelKeyIndex) {
const modelId = expectDefined(path[modelKeyIndex], `path entry at ${modelKeyIndex}`);
const normalizedModelId = normalizeAgentModelRefForConfig(modelId);
return normalizedModelId === modelId ? path : [
...path.slice(0, modelKeyIndex),
normalizedModelId,
...path.slice(modelKeyIndex + 1)
];
}
return path;
}
//#endregion
//#region src/cli/config-cli-roster.ts
/** Keeps the documented list input and keyed input on one mutable roster. */
var ConfigMutationAgentRoster = class {
constructor(root, sourceConfigBeforeMigrations) {
this.root = root;
const sourceRoster = readAgentRosterProperty(sourceConfigBeforeMigrations);
this.legacyOrder = sourceRoster?.kind === "list" ? parseLegacyAgentRoster(sourceRoster.value)?.order : void 0;
}
prepare(operation, merge) {
const path = operation.setPath;
if (path[0] !== "agents") return;
if (path.length === 1 && isRecord(operation.value) && Object.hasOwn(operation.value, "list") && Object.hasOwn(operation.value, "entries")) throw new Error("Set either agents.list or agents.entries in one value, not both.");
const submittedRoster = path.length === 1 ? readAgentRosterProperty({ agents: operation.value }) : void 0;
const kind = path.length === 1 ? submittedRoster?.kind : path[1];
const roster = readAgentRosterProperty(this.root);
if (!roster) this.legacyOrder = void 0;
const agents = this.root.agents;
if (kind !== "list" && kind !== "entries" || !isRecord(agents)) return;
if (path.length <= 2 && !merge && (kind === "entries" || operation.mutation !== "delete")) {
delete agents[kind === "entries" ? "list" : "entries"];
this.legacyOrder = void 0;
return;
}
if (kind === "entries") {
if (roster?.kind === "list") this.canonicalize(true);
return;
}
if (roster?.kind !== "entries") return;
if (!isRecord(roster.value) || Object.values(roster.value).some((entry) => !isRecord(entry) || Object.hasOwn(entry, "id"))) throw new Error("Cannot address agents.list while agents.entries contains invalid entries; correct the keyed roster first.");
const entries = roster.value;
const order = [.../* @__PURE__ */ new Set([...this.legacyOrder ?? [], ...Object.keys(entries)])].filter((id) => Object.hasOwn(entries, id));
const list = [];
for (const id of order) list.push({
...entries[id],
id
});
agents.list = list;
delete agents.entries;
}
writePath(path) {
if (path[0] !== "agents" || path[1] !== "list") return path;
if (path.length === 2) {
const roster = readAgentRosterProperty(this.root);
return roster?.kind === "list" && parseLegacyAgentRoster(roster.value) ? ["agents", "entries"] : path;
}
const entry = getAtPath(this.root, path.slice(0, 3));
const id = (entry.found ? parseLegacyAgentRoster([entry.value]) : void 0)?.order[0];
return id === void 0 ? path : [
"agents",
"entries",
id,
...path.slice(3)
];
}
finish() {
this.canonicalize(false);
}
canonicalize(required) {
const agents = this.root.agents;
const roster = readAgentRosterProperty(this.root);
if (!isRecord(agents) || roster?.kind !== "list") return;
const parsed = parseLegacyAgentRoster(roster.value);
if (!parsed) {
if (required) throw new Error("Cannot address agents.entries while agents.list contains invalid or duplicate ids; correct the list first.");
return;
}
this.legacyOrder = parsed.order;
agents.entries = parsed.entries;
delete agents.list;
}
};
//#endregion
//#region src/cli/config-model-validation.ts
function isPathPrefix(prefix, path) {
return prefix.length <= path.length && prefix.every((segment, index) => path[index] === segment);
}
function pathMayAffectTextModelRefs(path) {
if (path[0] !== "agents" || path.length === 1) return path[0] === "agents";
if (path[1] === "defaults") return path.length === 2 || path[2] === "model";
return path[1] === "entries" || path[1] === "list";
}
function collectTextModelConfigRefs(params) {
if (typeof params.model === "string") {
const value = params.model.trim();
return [{
path: params.path,
value,
...params.agentId ? { agentId: params.agentId } : {},
fallback: false
}];
}
if (!params.model || typeof params.model !== "object" || Array.isArray(params.model)) return [];
const model = params.model;
const refs = [];
if (typeof model.primary === "string") {
const value = model.primary.trim();
refs.push({
path: `${params.path}.primary`,
value,
...params.agentId ? { agentId: params.agentId } : {},
fallback: false
});
}
if (Array.isArray(model.fallbacks)) for (const [index, fallback] of model.fallbacks.entries()) {
if (typeof fallback !== "string") continue;
refs.push({
path: `${params.path}.fallbacks.${index}`,
value: fallback.trim(),
...params.agentId ? { agentId: params.agentId } : {},
fallback: true
});
}
return refs;
}
function collectTextModelRefs(config) {
const refs = collectTextModelConfigRefs({
model: config.agents?.defaults?.model,
path: "agents.defaults.model"
});
for (const { entry: agent, source } of listAgentEntriesWithSource(config)) {
const agentId = agent.id;
const agentPath = source.kind === "entries" ? `agents.entries.${source.key}` : `agents.list.${source.index}`;
refs.push(...collectTextModelConfigRefs({
model: agent.model,
path: `${agentPath}.model`,
agentId
}));
}
for (const ref of refs) {
if (ref.fallback) continue;
const authProfileId = splitTrailingAuthProfile(ref.value).profile;
if (authProfileId) ref.authProfileId = authProfileId;
}
return refs;
}
function modelRefComparisonKey(ref) {
if (ref.agentId) {
const modelOffset = ref.path.indexOf(".model");
const relativePath = modelOffset >= 0 ? ref.path.slice(modelOffset + 1) : ref.path;
return `agent:${normalizeAgentId(ref.agentId)}:${relativePath}`;
}
return `path:${ref.path}`;
}
function collectTouchedTextModelRefs(params) {
const listedAgentEntries = listAgentEntriesWithSource(params.config);
const defaultPrimaryPath = [
"agents",
"defaults",
"model",
"primary"
];
const defaultPrimaryTouched = params.touchedPaths.some((touchedPath) => isPathPrefix(touchedPath, defaultPrimaryPath) || isPathPrefix(defaultPrimaryPath, touchedPath));
const refs = collectTextModelRefs(params.config);
const previousRefs = params.previousConfig ? collectTextModelRefs(params.previousConfig) : void 0;
const previousRefsByIdentity = previousRefs ? new Map(previousRefs.map((ref) => [modelRefComparisonKey(ref), ref])) : void 0;
const previousDefaultAgentId = params.previousConfig ? tryResolveLegacyCompatibilityAgentId(params.previousConfig) : void 0;
const defaultPrimaryProviderChanged = defaultPrimaryTouched && (!previousRefs || previousDefaultAgentId === void 0 || resolveDefaultModelForAgent({ cfg: params.config }).provider !== resolveDefaultModelForAgent({
cfg: params.previousConfig,
agentId: previousDefaultAgentId
}).provider);
const touchedRefs = refs.filter((ref) => {
if (ref.fallback && defaultPrimaryProviderChanged) {
const previousRef = previousRefsByIdentity?.get(modelRefComparisonKey(ref));
const nextResolved = resolveCanonicalFallbackRef(params.config, ref.value);
const previousResolved = params.previousConfig && previousRef ? resolveCanonicalFallbackRef(params.previousConfig, previousRef.value) : void 0;
if (!nextResolved || !previousResolved || nextResolved.provider !== previousResolved.provider || nextResolved.model !== previousResolved.model) {
ref.dependency = true;
return true;
}
}
const refPath = ref.path.split(".");
const touched = params.touchedPaths.some((touchedPath) => isPathPrefix(touchedPath, refPath) || isPathPrefix(refPath, touchedPath));
if (!touched || !previousRefsByIdentity) return touched;
const previousRef = previousRefsByIdentity.get(modelRefComparisonKey(ref));
const ownerChanged = previousRef?.agentId !== ref.agentId;
if (ownerChanged) ref.dependency = true;
return previousRef?.value !== ref.value || ownerChanged;
});
const defaultRefs = refs.filter((ref) => ref.agentId === void 0);
if (defaultRefs.length === 0) return touchedRefs;
for (const { entry, source } of listedAgentEntries) {
const agentId = entry.id;
const agentEntryPath = [
"agents",
source.kind,
source.kind === "entries" ? source.key : String(source.index)
];
const agentModelPath = [...agentEntryPath, "model"];
if (!params.touchedPaths.some((touchedPath) => isPathPrefix(touchedPath, agentEntryPath) || isPathPrefix(agentEntryPath, touchedPath) || isPathPrefix(touchedPath, agentModelPath) || isPathPrefix(agentModelPath, touchedPath))) continue;
for (const defaultRef of defaultRefs) {
const inherits = defaultRef.fallback ? resolveAgentModelFallbacksOverride(params.config, agentId) === void 0 : resolveAgentExplicitModelPrimary(params.config, agentId) === void 0;
const previouslyInherited = (params.previousConfig ? listAgentEntries(params.previousConfig) : []).some((previousEntry) => normalizeAgentId(previousEntry.id) === normalizeAgentId(agentId)) && params.previousConfig ? defaultRef.fallback ? resolveAgentModelFallbacksOverride(params.previousConfig, agentId) === void 0 : resolveAgentExplicitModelPrimary(params.previousConfig, agentId) === void 0 : false;
if (inherits && !previouslyInherited) touchedRefs.push({
...defaultRef,
agentId,
dependency: true
});
}
}
return touchedRefs;
}
function resolveCanonicalPrimaryRef(config, value) {
const validationConfig = {
...config,
agents: {
...config.agents,
defaults: {
...config.agents?.defaults,
model: value
}
}
};
const resolved = resolveConfiguredModelRef({
cfg: validationConfig,
defaultProvider: DEFAULT_PROVIDER,
defaultModel: "",
allowPluginNormalization: true
});
return resolved.model ? resolved : void 0;
}
function resolveFallbackRef(config, value) {
const defaultProvider = resolveDefaultModelForAgent({ cfg: config }).provider;
return resolveModelRefFromString({
cfg: config,
raw: value,
defaultProvider,
aliasIndex: buildModelAliasIndex({
cfg: config,
defaultProvider,
allowPluginNormalization: true
}),
allowPluginNormalization: true
});
}
function resolveCanonicalFallbackRef(config, value) {
return resolveFallbackRef(config, value)?.ref;
}
function hasUnresolvedInheritedFallbackProvider(config, ref, unresolvedPaths) {
if (!ref.fallback || ref.value.includes("/") || !unresolvedPaths.has("agents.defaults.model") && !unresolvedPaths.has("agents.defaults.model.primary")) return false;
const primary = resolveAgentModelPrimaryValue(config.agents?.defaults?.model);
if (!primary) return false;
const primaryModel = splitTrailingAuthProfile(primary).model;
const slash = primaryModel.indexOf("/");
const provider = slash > 0 ? primaryModel.slice(0, slash) : primaryModel;
const fallback = resolveFallbackRef(config, ref.value);
return Boolean(fallback && !fallback.alias && containsEnvVarReference(provider));
}
function expandInheritedDefaultRefs(config, refs) {
const agentEntries = listAgentEntries(config);
const defaultAgentId = tryResolveLegacyCompatibilityAgentId(config);
const expanded = [];
const seen = /* @__PURE__ */ new Set();
const push = (ref) => {
const key = `${ref.path}\u0000${ref.agentId ?? ""}`;
if (!seen.has(key)) {
seen.add(key);
expanded.push(ref);
}
};
for (const ref of refs) {
if (ref.agentId !== void 0) {
push(ref);
continue;
}
if (defaultAgentId) {
if (!agentEntries.some((entry) => normalizeAgentId(entry.id) === normalizeAgentId(defaultAgentId)) || (ref.fallback ? resolveAgentModelFallbacksOverride(config, defaultAgentId) === void 0 : resolveAgentExplicitModelPrimary(config, defaultAgentId) === void 0)) push(ref);
}
for (const { id: agentId } of agentEntries) {
if (defaultAgentId && normalizeAgentId(agentId) === normalizeAgentId(defaultAgentId)) continue;
if (ref.fallback ? resolveAgentModelFallbacksOverride(config, agentId) === void 0 : resolveAgentExplicitModelPrimary(config, agentId) === void 0) push({
...ref,
agentId
});
}
}
return expanded;
}
function modelRefEnvSourcePath(path) {
return path.replace(/\.list\.(\d+)/u, ".list[$1]").replace(/\.fallbacks\.(\d+)$/u, ".fallbacks[$1]");
}
function validateModelRefSyntax(config, ref, unresolvedPaths) {
if (!ref.value) return "Model reference is empty";
if (unresolvedPaths.has(modelRefEnvSourcePath(ref.path))) return "Model reference contains an unresolved environment variable";
return (ref.fallback ? resolveCanonicalFallbackRef(config, ref.value) : resolveCanonicalPrimaryRef(config, ref.value)) ? void 0 : "Invalid model reference or configured model alias target";
}
async function createRuntimeModelRefResolver() {
const [agentScope, modelSelection] = await Promise.all([import("./agent-scope-BHLMs19p.js"), import("./model-selection-DEDE6TxI.js")]);
let modelModules;
const loadModelModules = () => modelModules ??= Promise.all([import("./model-CokXeqgU.js"), import("./prepared-model-runtime-D7Vr1U41.js")]);
return async ({ config, ref }) => {
const resolvedRef = ref.fallback ? resolveCanonicalFallbackRef(config, ref.value) : resolveCanonicalPrimaryRef(config, ref.value);
if (!resolvedRef) return `Unknown model: ${ref.value}`;
const { provider, model } = resolvedRef;
if (modelSelection.isCliProvider(provider, config)) return;
const targetAgentId = ref.agentId ?? agentScope.tryResolveLegacyCompatibilityAgentId(config) ?? agentScope.resolveDefaultAgentId(config);
const agentDir = agentScope.resolveAgentDir(config, targetAgentId);
const workspaceDir = agentScope.resolveAgentWorkspaceDir(config, targetAgentId);
const [modelRuntime, preparedRuntime] = await loadModelModules();
const lease = await preparedRuntime.acquireReadOnlyPreparedModelRuntime({
agentId: targetAgentId,
agentDir,
config,
workspaceDir,
loadRuntimePlugins: true,
runtimePluginSelections: [{
provider,
modelId: model,
agentId: targetAgentId
}]
});
try {
const stores = lease.snapshot.createStores();
const resolution = await modelRuntime.resolveModelAsync(provider, model, agentDir, config, {
...stores,
agentId: targetAgentId,
allowBundledStaticCatalogFallback: true,
...ref.authProfileId ? { authProfileId: ref.authProfileId } : {},
preparedModelRuntime: lease.snapshot,
workspaceDir
});
return resolution.model ? void 0 : resolution.error ?? `Unknown model: ${provider}/${model}`;
} finally {
lease.release();
}
};
}
function formatModelRefError(ref, error, authoredValue = ref.value, options) {
const safeError = options?.suppressDetail || authoredValue !== ref.value ? "Unable to resolve authored model reference" : error;
const detail = safeError.endsWith(".") ? safeError : `${safeError}.`;
return `Cannot set model reference "${authoredValue}" at ${ref.path}: ${detail} Run ${formatCliCommand("openclaw models list")} to list available models.`;
}
async function checkTouchedTextModelRefs(params) {
if (!params.touchedPaths.some(pathMayAffectTextModelRefs)) return {
refsChecked: 0,
refsTotal: 0,
errors: []
};
const config = hasAgentRosterProperty(params.config) ? params.config : migratePersistedImplicitMainRoster(params.config).config;
const previousConfig = params.previousConfig && !hasAgentRosterProperty(params.previousConfig) ? migratePersistedImplicitMainRoster(params.previousConfig).config : params.previousConfig;
const authoredRefs = collectTouchedTextModelRefs({
...params,
config,
previousConfig
});
const authoredValuesByPath = new Map(collectTextModelRefs(params.config).map((ref) => [ref.path, ref.value]));
const previousAuthoredValuesByPath = new Map(collectTextModelRefs(params.previousConfig ?? {}).map((ref) => [ref.path, ref.value]));
let validationConfig;
let validationPreviousConfig;
const unresolvedPaths = /* @__PURE__ */ new Set();
try {
const env = params.env ?? process.env;
validationConfig = resolveConfigEnvVars(params.config, env, { onMissing: ({ configPath }) => unresolvedPaths.add(configPath) });
validationPreviousConfig = params.previousConfig ? resolveConfigEnvVars(params.previousConfig, env, { onMissing: () => {} }) : void 0;
} catch (cause) {
const detail = cause instanceof Error ? cause.message : String(cause);
return {
refsChecked: 0,
refsTotal: authoredRefs.length,
errors: [`Unable to validate changed model references before writing: ${detail}`]
};
}
const validationValuesByPath = new Map(collectTextModelRefs(validationConfig).map((ref) => [ref.path, ref.value]));
const modelEnvWasExpanded = [...authoredValuesByPath].some(([path, value]) => validationValuesByPath.get(path) !== value);
const formatError = (ref, error) => {
const redactDependency = Boolean(params.redactDependencyValues && ref.dependency);
return formatModelRefError(ref, error, redactDependency ? "<configured model reference>" : authoredValuesByPath.get(ref.path), { suppressDetail: modelEnvWasExpanded || redactDependency });
};
const validationRefsByPath = new Map(collectTextModelRefs(validationConfig).map((ref) => [ref.path, ref]));
const validationRosterConfig = hasAgentRosterProperty(validationConfig) ? validationConfig : migratePersistedImplicitMainRoster(validationConfig).config;
const validationPreviousRosterConfig = validationPreviousConfig && !hasAgentRosterProperty(validationPreviousConfig) ? migratePersistedImplicitMainRoster(validationPreviousConfig).config : validationPreviousConfig;
const refsByKey = new Map(collectTouchedTextModelRefs({
config: validationRosterConfig,
previousConfig: validationPreviousRosterConfig,
touchedPaths: params.touchedPaths
}).map((ref) => [modelRefComparisonKey(ref), ref]));
for (const authoredRef of authoredRefs) {
if (authoredRef.dependency && previousAuthoredValuesByPath.get(authoredRef.path) === authoredRef.value) continue;
const validationRef = validationRefsByPath.get(authoredRef.path);
if (!validationRef) continue;
const key = modelRefComparisonKey(validationRef);
const expandedRef = refsByKey.get(key);
refsByKey.set(key, {
...validationRef,
...authoredRef.dependency || expandedRef?.dependency ? { dependency: true } : {}
});
}
const refs = expandInheritedDefaultRefs(validationRosterConfig, [...refsByKey.values()]);
if (refs.length === 0) return {
refsChecked: 0,
refsTotal: 0,
errors: []
};
const validatedRefs = refs.filter((ref) => !hasUnresolvedInheritedFallbackProvider(config, ref, unresolvedPaths)).map((ref) => ({
ref,
error: validateModelRefSyntax(validationConfig, ref, unresolvedPaths)
}));
const syntaxFailures = validatedRefs.filter((entry) => Boolean(entry.error));
const refsToResolve = validatedRefs.filter((entry) => !entry.error).map((entry) => entry.ref);
const errors = syntaxFailures.map(({ ref, error }) => formatError(ref, error));
if (refsToResolve.length === 0) return {
refsChecked: syntaxFailures.length,
refsTotal: refs.length,
errors
};
let resolveModelRef = params.resolveModelRef;
if (!resolveModelRef) try {
resolveModelRef = await (params.createModelRefResolver ?? createRuntimeModelRefResolver)();
} catch (cause) {
const detail = modelEnvWasExpanded || Boolean(params.redactDependencyValues && refs.some((ref) => ref.dependency)) ? "model resolver setup failed" : cause instanceof Error ? cause.message : String(cause);
return {
refsChecked: syntaxFailures.length,
refsTotal: refs.length,
errors: [...errors, `Unable to validate changed model references before writing: ${detail}`]
};
}
let refsChecked = syntaxFailures.length;
for (const ref of refsToResolve) {
let error;
try {
error = await resolveModelRef({
config: validationConfig,
ref
});
refsChecked += 1;
} catch (cause) {
const detail = cause instanceof Error ? cause.message : String(cause);
errors.push(formatError(ref, `Unable to validate model reference: ${detail}`));
continue;
}
if (!error) continue;
errors.push(formatError(ref, error));
}
return {
refsChecked,
refsTotal: refs.length,
errors
};
}
//#endregion
//#region src/cli/config-cli-validation.ts
function formatInvalidConfigRepairHint(snapshot, doctorMessage) {
return isPluginPackagingRuntimeOutputInvalidConfigSnapshot(snapshot) ? formatPluginPackagingRuntimeOutputRecoveryHint() : `Run \`${formatCliCommand("openclaw doctor --fix")}\` ${doctorMessage}`;
}
function ensureValidConfigSnapshotForCli(snapshot, runtime, options = {}) {
if (snapshot.valid) return;
if (options.json) {
writeRuntimeJson(runtime, {
...formatCliJsonFailure(`OpenClaw config is invalid: ${shortenHomePath(snapshot.path)}`),
issues: normalizeConfigIssues(snapshot.issues)
});
exitCliAfterOutput(runtime, 1);
}
runtime.error(`OpenClaw config is invalid: ${shortenHomePath(snapshot.path)}`);
for (const line of renderConfigValidationIssueLines(snapshot)) runtime.error(line);
runtime.error(formatInvalidConfigRepairHint(snapshot, "to repair, then retry."));
exitCliAfterOutput(runtime, 1);
}
async function loadValidConfigForWrite(runtime = defaultRuntime) {
const prepared = await readConfigFileSnapshotForWrite();
ensureValidConfigSnapshotForCli(prepared.snapshot, runtime);
return prepared;
}
async function strictlyValidateConfigSnapshotForCli(snapshot, pluginMetadataSnapshot) {
if (!snapshot.valid) return snapshot;
const validated = validateConfigObjectRawWithPlugins(snapshot.sourceConfig, {
semanticValidation: "strict",
pluginMetadataSnapshot
});
const issues = validated.ok ? await collectConfigSecretProviderErrors({ config: snapshot.runtimeConfig }) : validated.issues;
return issues.length === 0 ? snapshot : {
...snapshot,
valid: false,
issues
};
}
function pathContains(parent, child) {
return parent.length <= child.length && parent.every((part, index) => part === child[index]);
}
function selectConfigMutationSecrets(config, operations) {
const paths = operations.map(({ setPath }) => setPath);
const changedProviders = /* @__PURE__ */ new Set();
const changedDefaults = /* @__PURE__ */ new Set();
let allProviders = false;
for (const path of paths) {
if (path[0] !== "secrets") continue;
if (path.length === 1 || path[1] === "providers") {
const alias = path[2];
if (alias === void 0) allProviders = true;
else changedProviders.add(alias);
}
if (path.length === 1 || path[1] === "defaults") changedDefaults.add(path[2] ?? "*");
}
const refsByKey = /* @__PURE__ */ new Map();
const record = (ref) => refsByKey.set(secretRefKey(ref), ref);
const defaults = config.secrets?.defaults;
const ownedPaths = [];
const overlaps = (targetPath) => paths.some((path) => pathContains(path, targetPath) || pathContains(targetPath, path));
for (const target of discoverConfigSecretTargets(config)) {
ownedPaths.push(target.pathSegments);
if (target.refPathSegments) ownedPaths.push(target.refPathSegments);
const { explicitRef, ref } = resolveSecretInputRef({
value: target.value,
refValue: target.refValue,
defaults
});
if (!ref) continue;
const usesDefault = !isSecretRef(explicitRef ? target.refValue : target.value);
if (allProviders || changedProviders.has(ref.provider) || overlaps(target.pathSegments) || target.refPathSegments && overlaps(target.refPathSegments) || usesDefault && (changedDefaults.has("*") || changedDefaults.has(ref.source))) record(ref);
}
const visit = (value, rootPath) => {
visitConfigValueTree(value, (candidate, path) => {
if (ownedPaths.some((ownedPath) => pathContains(ownedPath, path))) return false;
const ref = coerceSecretRef(candidate, defaults);
if (ref) {
record(ref);
return false;
}
return true;
}, rootPath);
};
for (const path of paths) visit(getAtPath(config, path).value, path);
const refs = [...refsByKey.values()];
return {
refs,
providerAliases: allProviders ? void 0 : /* @__PURE__ */ new Set([...changedProviders, ...refs.map((ref) => ref.provider)])
};
}
async function collectDryRunResolvabilityErrors(params) {
const failures = [];
for (const ref of params.refs) try {
await resolveSecretRefValue(ref, {
config: params.config,
env: process.env
});
} catch (err) {
failures.push({
kind: "resolvability",
message: formatErrorMessage(err),
ref: `${ref.source}:${ref.provider}:${ref.id}`
});
}
return failures;
}
function collectDryRunStaticErrorsForSkippedExecRefs(params) {
const failures = [];
for (const ref of params.refs) {
const id = ref.id.trim();
const refLabel = `${ref.source}:${ref.provider}:${id}`;
if (!id) {
failures.push({
kind: "resolvability",
message: "Error: Secret reference id is empty.",
ref: refLabel
});
continue;
}
if (!isValidExecSecretRefId(id)) {
failures.push({
kind: "resolvability",
message: `Error: ${formatExecSecretRefIdValidationMessage()} (ref: ${refLabel}).`,
ref: refLabel
});
continue;
}
const providerConfig = params.config.secrets?.providers?.[ref.provider];
if (!providerConfig) {
failures.push({
kind: "resolvability",
message: `Error: Secret provider "${ref.provider}" is not configured (ref: ${refLabel}).`,
ref: refLabel
});
continue;
}
if (providerConfig.source !== ref.source) failures.push({
kind: "resolvability",
message: `Error: Secret provider "${ref.provider}" has source "${providerConfig.source}" but ref requests "${ref.source}".`,
ref: refLabel
});
}
return failures;
}
function selectDryRunRefsForResolution(params) {
const refsToResolve = [];
const skippedExecRefs = [];
for (const ref of params.refs) (ref.source === "exec" && !params.allowExecInDryRun ? skippedExecRefs : refsToResolve).push(ref);
return {
refsToResolve,
skippedExecRefs
};
}
function collectStrictConfigErrors(config, pluginMetadataSnapshot) {
const validated = validateConfigObjectRawWithPlugins(config, {
semanticValidation: "strict",
pluginMetadataSnapshot
});
if (validated.ok) return [];
return formatConfigIssueLines(validated.issues, "-", { normalizeRoot: true }).map((message) => ({
kind: "schema",
message
}));
}
function assertStrictConfigForMutation(config, pluginMetadataSnapshot) {
const errors = collectStrictConfigErrors(config, pluginMetadataSnapshot);
if (errors.length === 0) return;
throw new Error(["Config validation failed.", ...errors.map((error) => `- ${error.message}`)].join("\n"));
}
async function collectConfigSecretProviderErrors(params) {
const providers = params.config.secrets?.providers ?? {};
const issues = [];
let manifestRegistry;
for (const [alias, provider] of Object.entries(providers)) {
if (params.selection?.providerAliases && !params.selection.providerAliases.has(alias)) continue;
const providerPath = `secrets.providers.${alias}`;
if (isPluginIntegrationSecretProviderConfig(provider)) {
if (!params.selection) continue;
manifestRegistry ??= loadPluginMetadataSnapshot({
config: params.config,
env: process.env
}).manifestRegistry;
const resolved = resolveSecretProviderIntegrationConfig({
manifestRegistry,
providerAlias: alias,
providerConfig: provider,
config: params.config,
env: process.env
});
if (!resolved.ok) issues.push({
path: providerPath,
message: resolved.reason
});
} else if (isRecord(provider) && "command" in provider) try {
await assertSecureExecCommandPath({
command: provider.command,
label: `${providerPath}.command`,
trustedDirs: provider.trustedDirs
});
} catch (err) {
issues.push({
path: `${providerPath}.command`,
message: formatErrorMessage(err)
});
}
}
return issues;
}
function dedupeDryRunErrors(errors) {
const deduped = [];
const seen = /* @__PURE__ */ new Set();
for (const error of errors) {
const key = error.kind === "resolvability" ? `${error.kind}\u0000${error.ref ?? ""}\u0000${error.message}` : `${error.kind}\u0000${error.message}`;
if (!seen.has(key)) {
seen.add(key);
deduped.push(error);
}
}
return deduped;
}
/** Validates one final candidate and decides whether the runner may preview, skip, or write it. */
async function validateConfigMutation(params) {
const { config, operations, options, pluginMetadataSnapshot } = params;
const policyIssues = formatConfigIssueLines(collectUnsupportedSecretRefPolicyIssues(config), "", { normalizeRoot: true }).map((line) => line.trim());
const selection = selectConfigMutationSecrets(config, operations);
const providerErrors = formatConfigIssueLines(await collectConfigSecretProviderErrors({
config,
selection
}), "").map((message) => ({
kind: "schema",
message
}));
if (!options.dryRun) {
if (policyIssues.length > 0) throw new Error([
"Config policy validation failed: unsupported SecretRef usage was detected.",
...policyIssues.slice(0, 5).map((issue) => `- ${issue}`),
...policyIssues.length > 5 ? [`- ... ${policyIssues.length - 5} more`] : []
].join("\n"));
if (providerErrors.length > 0) throw new Error(["Config validation failed: SecretRef provider configuration is invalid.", ...providerErrors.map((error) => `- ${error.message}`)].join("\n"));
if (params.unchanged) {
assertStrictConfigForMutation(config, pluginMetadataSnapshot);
return { kind: "unchanged" };
}
}
const modelCheck = await checkTouchedTextModelRefs({
config,
previousConfig: params.previousConfig,
touchedPaths: operations.map(({ setPath }) => setPath),
redactDependencyValues: true
});
if (!options.dryRun) {
if (modelCheck.errors[0]) throw new Error(modelCheck.errors[0]);
return { kind: "write" };
}
const inputModes = uniqueValues(operations.map(({ inputMode }) => inputMode));
const checksRefs = inputModes.some((mode) => mode !== "value");
const requiresFullSchema = operations.some((operation) => operation.inputMode === "unset" || operation.inputMode === "json" && operation.schemaValidated !== true);
const { refsToResolve, skippedExecRefs } = selectDryRunRefsForResolution({
refs: checksRefs ? selection.refs : [],
allowExecInDryRun: Boolean(options.allowExec)
});
const errors = modelCheck.errors.map((message) => ({
kind: "model",
message
}));
if (!requiresFullSchema) errors.push(...policyIssues.map((message) => ({
kind: "schema",
message
})));
errors.push(...providerErrors);
if (requiresFullSchema) errors.push(...collectStrictConfigErrors(config, pluginMetadataSnapshot));
if (checksRefs) errors.push(...collectDryRunStaticErrorsForSkippedExecRefs({
refs: skippedExecRefs,
config
}), ...await collectDryRunResolvabilityErrors({
refs: refsToResolve,
config
}));
const failures = dedupeDryRunErrors(errors);
return {
kind: "dry-run",
result: {
ok: failures.length === 0,
operations: operations.length,
configPath: params.configPath,
inputModes,
checks: {
schema: requiresFullSchema || policyIssues.length > 0 || providerErrors.length > 0,
resolvability: checksRefs || modelCheck.refsTotal > 0,
resolvabilityComplete: (checksRefs || modelCheck.refsTotal > 0) && skippedExecRefs.length === 0 && modelCheck.refsChecked === modelCheck.refsTotal
},
refsChecked: refsToResolve.length + modelCheck.refsChecked,
skippedExecRefs: skippedExecRefs.length,
...failures.length > 0 ? { errors: failures } : {}
}
};
}
//#endregion
//#region src/cli/config-set-dryrun.ts
var ConfigSetDryRunValidationError = class extends Error {
constructor(result) {
super("config set dry-run validation failed");
this.result = result;
this.name = "ConfigSetDryRunValidationError";
}
};
function printConfigDryRunResult(result, runtime, json) {
if (!result.ok) {
if (json) throw new ConfigSetDryRunValidationError(result);
throw new Error(formatDryRunFailureMessage({
errors: result.errors ?? [],
skippedExecRefs: result.skippedExecRefs
}));
}
if (json) {
writeRuntimeJson(runtime, result);
return;
}
if (!result.checks.schema && !result.checks.resolvability) runtime.log(info("Dry run note: value mode does not run schema/resolvability checks. Use --strict-json, builder flags, or batch mode to enable validation checks."));
if (result.skippedExecRefs > 0) runtime.log(info(formatSkippedExecRefs(result.skippedExecRefs)));
runtime.log(info(`Dry run successful: ${result.operations} update(s) validated against ${shortenHomePath(result.configPath)}.`));
}
function formatSkippedExecRefs(count) {
return `Dry run note: skipped ${count} exec SecretRef resolvability check(s). Re-run with --allow-exec to execute exec providers during dry-run.`;
}
function formatDryRunFailureMessage(params) {
const missingPathErrors = params.errors.filter((error) => error.kind === "missing-path");
const schemaErrors = params.errors.filter((error) => error.kind === "schema");
const resolveErrors = params.errors.filter((error) => error.kind === "resolvability");
const modelErrors = params.errors.filter((error) => error.kind === "model");
const lines = missingPathErrors.map((error) => error.message);
if (schemaErrors.length > 0) lines.push("Dry run failed: config schema validation failed.", ...schemaErrors.map((error) => `- ${error.message}`));
if (resolveErrors.length > 0) {
lines.push(`Dry run failed: ${resolveErrors.length} SecretRef assignment(s) could not be resolved.`, ...resolveErrors.slice(0, 5).map((error) => `- ${error.ref ?? "<unknown-ref>"} -> ${error.message}`));
if (resolveErrors.length > 5) lines.push(`- ... ${resolveErrors.length - 5} more`);
}
if (modelErrors.length > 0) lines.push("Dry run failed: model reference validation failed.", ...modelErrors.map((error) => `- ${error.message}`));
if (params.skippedExecRefs > 0) lines.push(formatSkippedExecRefs(params.skippedExecRefs));
return lines.join("\n");
}
//#endregion
//#region src/cli/config-cli-runner.ts
const GATEWAY_AUTH_MODE_PATH = [
"gateway",
"auth",
"mode"
];
const PLUGIN_INSTALL_RECORD_PATH_PREFIX = ["plugins", "installs"];
function pathStartsWith(path, prefix) {
return prefix.every((segment, index) => path[index] === segment);
}
function pathEquals(path, expected) {
return path.length === expected.length && path.every((segment, index) => segment === expected[index]);
}
function valueHasAutoManagedChild(value, childPath) {
let cursor = value;
for (const segment of childPath) {
if (!isRecord(cursor)) return false;
if (!Object.hasOwn(cursor, segment)) return false;
cursor = cursor[segment];
}
return cursor !== void 0;
}
function operationClobbersAncestorChild(operation, managedPath, merge) {
if (operation.mutation === "delete") return true;
const childPath = managedPath.slice(operation.requestedPath.length);
return operation.mutation === "merge" || merge && operation.mutation !== "replace" ? valueHasAutoManagedChild(operation.value, childPath) : true;
}
function findAutoManagedMetaTargets(operations, merge) {
const matches = [];
const seen = /* @__PURE__ */ new Set();
const record = (path) => {
const key = toDotPath(path);
if (!seen.has(key)) {
seen.add(key);
matches.push([...path]);
}
};
for (const operation of operations) {
if (AUTO_MANAGED_CONFIG_META_PATHS.some((path) => pathStartsWith(operation.requestedPath, path))) {
record(operation.requestedPath);
continue;
}
for (const managedPath of AUTO_MANAGED_CONFIG_META_PATHS) if (operation.requestedPath.length < managedPath.length && pathStartsWith(managedPath, operation.requestedPath) && operationClobbersAncestorChild(operation, managedPath, merge)) record(managedPath);
}
return matches;
}
function formatAutoManagedMetaError(paths) {
const targets = paths.map(toDotPath);
return [
`${targets.length === 1 ? targets[0] : targets.join(", ")} is auto-managed by OpenClaw and cannot be edited; the value would be overwritten on the next config write.`,
"",
"These fields are stamped on every config write to record the OpenClaw version and timestamp that produced the file."
].join("\n");
}
function pruneInactiveGatewayAuthCredentials(params) {
const touchedMode = params.operations.some(({ requestedPath }) => pathEquals(requestedPath, GATEWAY_AUTH_MODE_PATH));
const gateway = params.root.gateway;
if (!touchedMode || !isRecord(gateway)) return [];
const auth = gateway.auth;
if (!isRecord(auth)) return [];
const mode = typeof auth.mode === "string" ? auth.mode.trim() : "";
const removedPaths = [];
const remove = (key) => {
if (Object.hasOwn(auth, key)) {
delete auth[key];
removedPaths.push(`gateway.auth.${key}`);
}
};
if (mode === "token") remove("password");
else if (mode === "password") remove("token");
else if (mode === "trusted-proxy") {
remove("token");
remove("password");
}
return removedPaths;
}
function collectChangedLeafPaths(value, prefix) {
if (!isRecord(value)) return [prefix];
const entries = Object.entries(value);
return entries.length === 0 ? [prefix] : entries.flatMap(([key, child]) => collectChangedLeafPaths(child, prefix ? `${prefix}.${key}` : key));
}
function expandActualChangedPaths(actualPaths, requestedPaths, before, after) {
const expanded = /* @__PURE__ */ new Set();
for (const actualPath of actualPaths) {
const descendants = requestedPaths.filter((requested) => requested !== actualPath && requested.startsWith(`${actualPath}.`));
if (descendants.length > 0) {
descendants.forEach((path) => expanded.add(path));
continue;
}
const path = actualPath === "<root>" ? [] : actualPath.split(".");
const beforeValue = getAtPath(before, path);
const afterValue = getAtPath(after, path);
const changedValue = beforeValue.found && !afterValue.found ? beforeValue : afterValue;
(beforeValue.found !== afterValue.found ? collectChangedLeafPaths(changedValue.value, actualPath) : [actualPath]).forEach((entry) => expanded.add(entry));
}
return [...expanded];
}
function configApplyHintForOperations(operations, beforeConfig, afterConfig) {
const requestedPaths = operations.map(({ requestedPath }) => toDotPath(requestedPath));
const paths = expandActualChangedPaths(diffConfigPaths(beforeConfig, afterConfig), requestedPaths, beforeConfig, afterConfig);
if (paths.length === 0) return "No gateway restart needed.";
if (paths.some((path) => path === "plugins.entries" || path.startsWith("plugins.entries."))) return "Restart the gateway to apply.";
const plan = buildGatewayReloadPlan(paths, { candidateConfig: afterConfig });
if (plan.restartGateway || plan.hotReasons.length > 0 && resolveGatewayReloadSettings(afterConfig).mode === "off") return "Restart the gateway to apply.";
return plan.hotReasons.length > 0 ? "Change will apply without restarting the gateway." : "No gateway restart needed.";
}
async function loadMutationSchema() {
try {
return (await readBestEffortRuntimeConfigSchema()).schema;
} catch {
return;
}
}
function assertConfigSetCurrentExpectation(params) {
const current = getAtPath(params.authoredConfig, params.operation.setPath);
if (!(params.expectation.kind === "absent" ? !current.found : current.found && isDeepStrictEqual(current.value, params.expectation.value))) throw new ConfigMutationConflictError("conditional config set expectation did not match the authored config", { retryable: false });
}
function assertConfigSetCurrentExpectationPath(params) {
if (!pathEquals(params.operation.requestedPath, params.writePath)) throw new Error("conditional config set requires a direct, non-redirected config path");
}
async function runConfigOperations(params) {
const { runtime, operations, options } = params;
if (operations.some(({ requestedPath }) => pathStartsWith(requestedPath, PLUGIN_INSTALL_RECORD_PATH_PREFIX))) throw new Error(formatPluginInstallConfigSetError());
const autoManagedTargets = findAutoManagedMetaTargets(operations, options.merge);
if (autoManagedTargets.length > 0) throw new Error(formatAutoManagedMetaError(autoManagedTargets));
const mutationStart = await loadValidConfigForWrite(runtime);
const { snapshot } = mutationStart;
const currentExpectation = params.currentExpectation;
let assertCurrentExpectation;
if (currentExpectation) {
const expectationOperation = operations[0];
if (!expectationOperation) throw new Error("conditional config set requires one resolved operation");
assertCurrentExpectation = () => {
assertConfigSetCurrentExpectation({
authoredConfig: snapshot.resolved,
operation: expectationOperation,
expectation: currentExpectation
});
};
}
const next = structuredClone(snapshot.resolved);
const currentConfig = normalizeConfigMutationModelRefs(snapshot.resolved);
const mutationSchema = await loadMutationSchema();
const roster = new ConfigMutationAgentRoster(next, snapshot.sourceConfigBeforeMigrations);
let unsetPaths = [];
const explicitSetPaths = [];
const appliedOperations = [];
const recordOperation = (operation) => {
const writePath = roster.writePath(operation.setPath);
const setPath = normalizeConfigMutationExplicitSetPath(operation.setPath[1] === "list" && writePath[1] === "entries" && writePath.length === 4 && writePath[3] === "id" ? writePath.slice(0, 3) : writePath);
appliedOperations.push({
...operation,
setPath
});
return writePath;
};
for (const operation of operations) {
const merge = operation.mutation === "merge" || options.merge && operation.mutation !== "replace";
roster.prepare(operation, Boolean(merge));
if (currentExpectation) assertConfigSetCurrentExpectationPath({
operation,
writePath: roster.writePath(operation.setPath)
});
if (operation.mutation === "delete") {
const writePath = recordOperation(operation);
const unsetResult = unsetAtPath(next, operation.setPath);
if (!unsetResult.removed && operation.inputMode === "unset") {
const requestedPath = formatConfigSetPath(operation.requestedPath, operation.pathTokens);
const runtimeOnly = getAtPath(snapshot.runtimeConfig, operation.setPath).found;
const message = formatConfigUnsetMissingPathMessage({
path: requestedPath,
runtimeOnly
});
if (options.dryRun && options.json) throw new ConfigSetDryRunValidationError({
ok: false,
operations: 1,
configPath: snapshot.path,
inputModes: ["unset"],
checks: {
schema: false,
resolvability: false,
resolvabilityComplete: false
},
refsChecked: 0,
skippedExecRefs: 0,
errors: [{
kind: "missing-path",
message: runtimeOnly ? message : `Config path not found: ${requestedPath}. Nothing was changed.`
}]
});
if (!options.dryRun) assertStrictConfigForMutation(currentConfig, mutationStart.writeOptions.basePluginMetadataSnapshot);
throw new Error(message);
}
if (!unsetResult.removed || unsetResult.leafContainer !== "array") unsetPaths.push(writePath);
continue;
}
const pathOptions = {
numericObjectKeys: params.successMode === "patch",
pathTokens: operation.pathTokens,
quotedNumericSegments: operation.quotedNumericSegments,
schema: mutationSchema
};
if (merge) mergeAtPath(next, operation.setPath, operation.value, pathOptions);
else {
assertNonDestructiveReplacement({
root: next,
path: operation.setPath,
value: operation.value,
allowReplace: options.replace || operation.mutation === "replace"
});
setAtPath(next, operation.setPath, operation.value, pathOptions);
}
explicitSetPaths.push(recordOperation(operation));
}
roster.finish();
unsetPaths = unsetPaths.filter((path) => !getAtPath(next, path).found);
const removedGatewayAuthPaths = pruneInactiveGatewayAuthCredentials({
root: next,
operations
});
let nextConfig = normalizeConfigMutationModelRefs(next);
const normalizedExplicitSetPaths = explicitSetPaths.map(normalizeConfigMutationExplicitSetPath);
if (options.dryRun) nextConfig = prepareConfigWriteTopology({
snapshot,
pluginMetadataSnapshot: mutationStart.writeOptions.basePluginMetadataSnapshot,
nextConfig,
options: { explicitSetPaths: normalizedExplicitSetPaths },
unsetPaths: resolveManagedUnsetPathsForWrite(unsetPaths),
env: process.env
}).nextConfig;
const validation = await validateConfigMutation({
config: nextConfig,
previousConfig: currentConfig,
operations: appliedOperations,
options,
configPath: snapshot.path,
unchanged: params.successMode === "set" && isDeepStrictEqual(currentConfig, nextConfig),
pluginMetadataSnapshot: mutationStart.writeOptions.basePluginMetadataSnapshot
});
if (validation.kind === "dry-run") {
printConfigDryRunResult(validation.result, runtime, options.json);
return;
}
if (validation.kind === "unchanged") {
assertCurrentExpectation?.();
runtime.log(info("No change"));
return;
}
await replaceConfigFile({
nextConfig,
snapshot,
...snapshot.hash !== void 0 ? { baseHash: snapshot.hash } : {},
writeOptions: {
...mutationStart.writeOptions,
auditOrigin: "cli",
...assertCurrentExpectation || params.beforePersistentApply ? { assertConfigPathForWrite: () => {
mutationStart.writeOptions.assertConfigPathForWrite?.();
assertCurrentExpectation?.();
params.beforePersistentApply?.();
} } : {},
...unsetPaths.length > 0 ? { unsetPaths } : {},
...normalizedExplicitSetPaths.length > 0 ? { explicitSetPaths: normalizedExplicitSetPaths } : {}
}
});
if (removedGatewayAuthPaths.length > 0) runtime.log(info(`Removed inactive ${removedGatewayAuthPaths.join(", ")} for gateway.auth.mode=${nextConfig.gateway?.auth?.mode ?? "<unset>"}.`));
const hint = configApplyHintForOperations(operations, currentConfig, nextConfig);
if (params.successMode === "set" && operations.length === 1) {
const operation = operations[0];
const action = operation?.mutation === "delete" ? "Removed" : "Updated";
const requestedPath = formatConfigSetPath(operation?.requestedPath ?? [], operation?.pathTokens, nextConfig);
runtime.log(info(`${action} ${requestedPath}. ${hint}`));
} else if (params.successMode === "set") runtime.log(info(`Updated ${operations.length} config paths. ${hint}`));
else runtime.log(info(`Applied ${operations.length} config update(s). ${hint}`));
}
function handleConfigMutationError(params) {
if (params.err instanceof ExitError) throw params.err;
const isConflict = params.err instanceof ConfigMutationConflictError;
const detail = formatErrorMessage(params.err);
const message = isConflict ? `The config file changed while this command was writing (${detail}), so nothing was changed. Re-run the same command to pick up the new file and try again.` : detail;
if (params.options.dryRun && params.options.json) {
if (params.err instanceof ConfigSetDryRunValidationError) {
writeRuntimeJson(params.runtime, params.err.result);
exitCliAfterOutput(params.runtime, 1);
}
const result = {
ok: false,
operations: 0,
configPath: resolveConfigPath(),
inputModes: [],
checks: {
schema: false,
resolvability: false,
resolvabilityComplete: false
},
refsChecked: 0,
skippedExecRefs: 0,
errors: [{
kind: isConflict ? "conflict" : "schema",
message
}]
};
writeRuntimeJson(params.runtime, result);
params.runtime.error(danger(message));
exitCliAfterOutput(params.runtime, 1);
}
params.runtime.error(danger(message));
exitCliAfterOutput(params.runtime, 1);
}
//#endregion
//#region src/cli/config-cli.ts
const CONFIG_SET_DESCRIPTION = [
"Set config values by path (value mode, ref/provider builder mode, or batch JSON mode).",
"Examples:",
formatCliCommand("openclaw config set gateway.port 19001 --strict-json"),
formatCliCommand("openclaw config set channels.discord.token --ref-provider default --ref-source env --ref-id DISCORD_BOT_TOKEN"),
formatCliCommand("openclaw config set secrets.providers.vault --provider-source file --provider-path /etc/openclaw/secrets.json --provider-mode json"),
formatCliCommand("openclaw config set --batch-file ./config-set.batch.json --dry-run")
].join("\n");
const CONFIG_PATCH_DESCRIPTION = [
"Patch config from a JSON5 object in one validated write.",
"Objects merge recursively, arrays/scalars replace, and null deletes a path.",
"Examples:",
formatCliCommand("openclaw config patch --file ./openclaw.patch.json5 --dry-run"),
formatCliCommand("openclaw config patch --stdin")
].join("\n");
async function runConfigSet(opts) {
const runtime = opts.runtime ?? defaultRuntime;
try {
const currentExpectation = parseConfigSetCurrentExpectation(opts.cliOptions);
const operations = buildConfigSetOperations({
path: opts.path,
value: opts.value,
opts: opts.cliOptions
});
if (currentExpectation && operations.length !== 1) throw new Error("config set mode error: conditional expectations require exactly one resolved operation.");
await runConfigOperations({
runtime,
operations,
options: opts.cliOptions,
successMode: "set",
...currentExpectation ? { currentExpectation } : {},
...opts.beforePersistentApply ? { beforePersistentApply: opts.beforePersistentApply } : {}
});
} catch (err) {
handleConfigMutationError({
err,
runtime,
options: opts.cliOptions
});
}
}
async function runConfigPatch(opts) {
const runtime = opts.runtime ?? defaultRuntime;
try {
if (opts.cliOptions.allowExec && !opts.cliOptions.dryRun) throw configPatchModeError("--allow-exec requires --dry-run.");
if (opts.cliOptions.json && !opts.cliOptions.dryRun) throw configPatchModeError("--json requires --dry-run.");
await runConfigOperations({
runtime,
operations: await readConfigPatchOperations(opts.cliOptions),
options: opts.cliOptions,
successMode: "patch"
});
} catch (err) {
handleConfigMutationError({
err,
runtime,
options: opts.cliOptions
});
}
}
async function runConfigGet(opts) {
const runtime = opts.runtime ?? defaultRuntime;
try {
const parsedPath = parseConcreteConfigPath(opts.path);
const { snapshot, pluginMetadataSnapshot } = await readConfigFileSnapshotWithPluginMetadata({ observe: false });
ensureValidConfigSnapshotForCli(snapshot, runtime, { json: opts.json });
if (!pluginMetadataSnapshot) throw new Error("Config plugin metadata unavailable; refusing to display config values.");
const { schema, uiHints } = buildRuntimeConfigSchemaFromRegistry(pluginMetadataSnapshot.manifestRegistry, snapshot.sourceConfig);
const res = getAtPath(redactConfigObject(snapshot.config, uiHints), parsedPath);
if (!res.found) {
const message = isConfigSchemaPath(schema, parsedPath) ? `Config path is valid but unset: ${opts.path}. The runtime default applies until you set an authored value with ${formatCliCommand(`openclaw config set ${quoteCliArg(opts.path)} <value>`)}.` : `Unknown config path: ${opts.path}. Run ${formatCliCommand("openclaw config schema")} to inspect valid paths.`;
if (opts.json) {
writeRuntimeJson(runtime, formatCliJsonFailure(message));
exitCliAfterOutput(runtime, 1);
}
runtime.error(danger(message));
exitCliAfterOutput(runtime, 1);
}
if (opts.json) writeRuntimeJson(runtime, res.value ?? null);
else if (typeof res.value === "string" || typeof res.value === "number" || typeof res.value === "boolean") writeRuntimeStdout(runtime, `${String(res.value)}\n`);
else writeRuntimeJson(runtime, res.value ?? null);
} catch (err) {
if (err instanceof ExitError) throw err;
if (opts.json) {
writeRuntimeJson(runtime, formatCliJsonFailure(err));
exitCliAfterOutput(runtime, 1);
}
runtime.error(danger(formatErrorMessage(err)));
exitCliAfterOutput(runtime, 1);
}
}
async function runConfigUnset(opts) {
const runtime = opts.runtime ?? defaultRuntime;
const cliOptions = opts.cliOptions ?? {};
try {
if (cliOptions.allowExec && !cliOptions.dryRun) throw new Error("--allow-exec can only be used with --dry-run.");
if (cliOptions.json && !cliOptions.dryRun) throw new Error("--json can only be used with --dry-run.");
const pathTokens = parseConcreteConfigPathTokens(opts.path);
await runConfigOperations({
runtime,
operations: [buildUnsetOperation(pathTokens.map(String), pathTokens)],
options: cliOptions,
successMode: "set"
});
} catch (err) {
handleConfigMutationError({
err,
runtime,
options: cliOptions
});
}
}
async function runConfigFile(opts) {
const runtime = opts.runtime ?? defaultRuntime;
try {
const path = resolveConfigPath();
if (opts.json) {
writeRuntimeJson(runtime, { path });
return;
}
writeRuntimeStdout(runtime, `${path}\n`);
} catch (err) {
runtime.error(danger(formatErrorMessage(err)));
exitCliAfterOutput(runtime, 1);
}
}
async function runConfigSchema(opts = {}) {
const runtime = opts.runtime ?? defaultRuntime;
try {
const schema = structuredClone((await readBestEffortRuntimeConfigSchema()).schema);
schema.properties = {
$schema: { type: "string" },
...schema.properties
};
writeRuntimeJson(runtime, schema);
} catch (err) {
runtime.error(danger(`Config schema error: ${formatErrorMessage(err)}`));
exitCliAfterOutput(runtime, 1);
}
}
async function runConfigValidate(opts = {}) {
const runtime = opts.runtime ?? defaultRuntime;
let outputPath = CONFIG_PATH ?? "openclaw.json";
try {
const read = await readConfigFileSnapshotWithPluginMetadata({ observe: false });
const snapshot = await strictlyValidateConfigSnapshotForCli(read.snapshot, read.pluginMetadataSnapshot);
outputPath = snapshot.path;
const shortPath = shortenHomePath(outputPath);
if (!snapshot.exists) {
if (opts.json) writeRuntimeJson(runtime, {
...formatCliJsonFailure("file not found"),
valid: false,
path: outputPath
}, 0);
else {
runtime.error(danger(`Config file not found: ${shortPath}`));
runtime.error(`Create one with ${formatCliCommand("openclaw onboard")} or run ${formatCliCommand("openclaw doctor --fix")}.`);
}
exitCliAfterOutput(runtime, 1);
}
if (!snapshot.valid) {
const issues = normalizeConfigIssues(snapshot.issues);
if (opts.json) writeRuntimeJson(runtime, {
...formatCliJsonFailure(`OpenClaw config is invalid: ${shortPath}`),
valid: false,
path: outputPath,
issues
});
else {
runtime.error(danger(`OpenClaw config is invalid: ${shortPath}`));
for (const line of renderConfigValidationIssueLines(snapshot, danger("×"))) runtime.error(` ${line}`);
runtime.error("");
runtime.error(formatInvalidConfigRepairHint(snapshot, "to repair, or fix the keys above manually."));
runtime.error(`Inspect with ${formatCliCommand("openclaw config validate")}.`);
}
exitCliAfterOutput(runtime, 1);
}
const warnings = normalizeConfigIssues(snapshot.warnings);
if (opts.json) writeRuntimeJson(runtime, {
valid: true,
path: outputPath,
warnings
}, 0);
else {
runtime.log(success(`Config valid: ${shortPath}`));
if (warnings.length > 0) {
runtime.log(warn(`${warnings.length} warning(s):`));
for (const line of formatConfigIssueLines(warnings, warn("!"), { normalizeRoot: true })) runtime.log(` ${line}`);
}
}
} catch (err) {
if (err instanceof ExitError) throw err;
if (opts.json) writeRuntimeJson(runtime, {
...formatCliJsonFailure(err),
valid: false,
path: outputPath
}, 0);
else runtime.error(danger(`Config validation error: ${formatErrorMessage(err)}`));
exitCliAfterOutput(runtime, 1);
}
}
function collectOption(value, previous) {
return [...previous, value];
}
function registerConfigCli(program) {
const cmd = program.command("config").description("Non-interactive config helpers (get/set/patch/unset/file/schema/validate). Run without subcommand for guided setup.").addHelpText("after", () => `\n${theme.muted("Docs:")} ${formatDocsLink("/cli/config", "docs.openclaw.ai/cli/config")}\n`).option("--section <section>", "Configuration sections for guided setup (repeatable). Use with no subcommand.", collectOption, []).action(async (opts) => {
const { configureCommandFromSectionsArg } = await import("./configure-CH-QPBaz.js");
await configureCommandFromSectionsArg(opts.section, defaultRuntime);
});
setCommandJsonMode(cmd, "output", ({ argv }) => isConfigMachineOutput(argv));
cmd.command("get").description("Get a config value by dot path").argument("<path>", "Config path (dot or bracket notation)").option("--json", "Output JSON", false).action(async (path, opts) => {
await runConfigGet({
path,
json: Boolean(opts.json)
});
});
setCommandJsonMode(cmd.command("set"), "parse-only", ({ argv }) => isConfigSetJsonParseOnly(argv)).description(CONFIG_SET_DESCRIPTION).argument("[path]", "Config path (dot or bracket notation)").argument("[value]", "Value (JSON/JSON5 or raw string)").option("--strict-json", "Strict JSON parsing (error instead of raw string fallback)", false).option("--json", "Legacy alias for --strict-json", false).option("--expect-current-absent", "Write only when the authored path is absent", false).option("--expect-current-json <json>", "Write only when the authored path exactly matches this strict JSON value").option("--dry-run", "Validate changes without writing openclaw.json (checks run in builder/json/batch modes; exec SecretRefs are skipped unless --allow-exec is set)", false).option("--allow-exec", "Dry-run only: allow exec SecretRef resolvability checks (may execute provider commands)", false).option("--merge", "Merge object/map values instead of replacing the target path", false).option("--replace", "Allow full replacement of protected map/list paths such as agents.defaults.models", false).option("--ref-provider <alias>", "SecretRef builder: provider alias").option("--ref-source <source>", "SecretRef builder: source (env|file|exec|store)").option("--ref-id <id>", "SecretRef builder: ref id").option("--provider-source <source>", "Provider builder: source (env|file|exec|store)").option("--provider-allowlist <envVar>", "Provider builder (env): allowlist entry (repeatable)", collectOption, []).option("--provider-path <path>", "Provider builder (file): path").option("--provider-mode <mode>", "Provider builder (file): mode (singleValue|json)").option("--provider-timeout-ms <ms>", "Provider builder (file|exec): timeout ms").option("--provider-max-bytes <bytes>", "Provider builder (file): max bytes").option("--provider-command <path>", "Provider builder (exec): absolute command path").option("--provider-arg <arg>", "Provider builder (exec): command arg (repeatable)", collectOption, []).option("--provider-no-output-timeout-ms <ms>", "Provider builder (exec): no-output timeout ms").option("--provider-max-output-bytes <bytes>", "Provider builder (exec): max output bytes").option("--provider-json-only", "Provider builder (exec): require JSON output", false).option("--provider-env <key=value>", "Provider builder (exec): env assignment (repeatable)", collectOption, []).option("--provider-pass-env <envVar>", "Provider builder (exec): pass host env var (repeatable)", collectOption, []).option("--provider-trusted-dir <path>", "Provider builder (exec): trusted directory (repeatable)", collectOption, []).option("--batch-json <json>", "Batch mode: JSON array of set operations").option("--batch-file <path>", "Batch mode: read JSON array of set operations from file").action(async (path, value, opts) => {
await runConfigSet({
path,
value,
cliOptions: opts
});
});
cmd.command("patch").description(CONFIG_PATCH_DESCRIPTION).option("--file <path>", "Read a JSON5 config patch object from file").option("--stdin", "Read a JSON5 config patch object from stdin", false).option("--dry-run", "Validate changes without writing openclaw.json (checks schema and SecretRef resolvability; exec SecretRefs are skipped unless --allow-exec is set)", false).option("--allow-exec", "Dry-run only: allow exec SecretRef resolvability checks (may execute provider commands)", false).option("--json", "Output dry-run result as JSON", false).option("--replace-path <path>", "Replace the object or array at this dot/bracket path instead of recursively applying it (repeatable)", collectOption, []).action(async (opts) => {
await runConfigPatch({ cliOptions: opts });
});
cmd.command("unset").description("Remove a config value by dot path").argument("<path>", "Config path (dot or bracket notation)").option("--dry-run", "validate the removal without writing the config file").option("--allow-exec", "allow exec SecretRef providers during --dry-run").option("--json", "print dry-run result as JSON").action(async (path, options) => {
await runConfigUnset({
path,
cliOptions: options
});
});
cmd.command("file").description("Print the active config file path").option("--json", "Output JSON", false).action((opts) => runConfigFile(opts));
cmd.command("schema").description("Print the JSON schema for openclaw.json").option("--json", "Output JSON", false).action(runConfigSchema);
cmd.command("validate").description("Validate the current config against the schema without starting the gateway").option("--json", "Output validation result as JSON", false).action(async (opts) => {
await runConfigValidate({ json: Boolean(opts.json) });
});
}
//#endregion
export { parseConcreteConfigPath as parseConfigSetPath, registerConfigCli, runConfigGet, runConfigPatch, runConfigSet, runConfigUnset };