2026-01-16 06:57:16 +00:00
import type { Command } from "commander" ;
2026-02-01 10:03:47 +09:00
import JSON5 from "json5" ;
2026-03-16 12:04:41 +00:00
import { OLLAMA_DEFAULT_BASE_URL } from "../agents/ollama-defaults.js" ;
2026-03-17 18:15:49 -05:00
import type { OpenClawConfig } from "../config/config.js" ;
2026-01-16 06:57:16 +00:00
import { readConfigFileSnapshot , writeConfigFile } from "../config/config.js" ;
2026-03-02 20:05:12 -05:00
import { formatConfigIssueLines , normalizeConfigIssues } from "../config/issue-format.js" ;
2026-03-02 13:45:51 +08:00
import { CONFIG_PATH } from "../config/paths.js" ;
2026-02-22 23:51:13 -08:00
import { isBlockedObjectKey } from "../config/prototype-keys.js" ;
2026-02-22 19:37:33 -05:00
import { redactConfigObject } from "../config/redact-snapshot.js" ;
2026-03-17 18:15:49 -05:00
import {
coerceSecretRef ,
isValidEnvSecretRefId ,
resolveSecretInputRef ,
type SecretProviderConfig ,
type SecretRef ,
type SecretRefSource ,
} from "../config/types.secrets.js" ;
import { validateConfigObjectRaw } from "../config/validation.js" ;
import { SecretProviderSchema } from "../config/zod-schema.core.js" ;
2026-03-02 13:45:51 +08:00
import { danger , info , success } from "../globals.js" ;
2026-02-18 01:34:35 +00:00
import type { RuntimeEnv } from "../runtime.js" ;
2026-01-16 06:57:16 +00:00
import { defaultRuntime } from "../runtime.js" ;
2026-03-17 18:15:49 -05:00
import {
formatExecSecretRefIdValidationMessage ,
2026-03-17 20:20:11 -05:00
isValidExecSecretRefId ,
2026-03-17 18:15:49 -05:00
isValidFileSecretRefId ,
isValidSecretProviderAlias ,
secretRefKey ,
validateExecSecretRefId ,
} from "../secrets/ref-contract.js" ;
import { resolveSecretRefValue } from "../secrets/resolve.js" ;
import {
discoverConfigSecretTargets ,
resolveConfigSecretTargetByPath ,
} from "../secrets/target-registry.js" ;
2026-01-16 06:57:16 +00:00
import { formatDocsLink } from "../terminal/links.js" ;
import { theme } from "../terminal/theme.js" ;
2026-01-23 03:43:32 +00:00
import { shortenHomePath } from "../utils.js" ;
2026-02-01 10:03:47 +09:00
import { formatCliCommand } from "./command-format.js" ;
2026-03-17 18:15:49 -05:00
import type {
ConfigSetDryRunError ,
ConfigSetDryRunInputMode ,
ConfigSetDryRunResult ,
} from "./config-set-dryrun.js" ;
import {
2026-03-17 18:28:46 -05:00
hasBatchMode ,
2026-03-17 18:15:49 -05:00
hasProviderBuilderOptions ,
hasRefBuilderOptions ,
parseBatchSource ,
type ConfigSetBatchEntry ,
type ConfigSetOptions ,
} from "./config-set-input.js" ;
import { resolveConfigSetMode } from "./config-set-parser.js" ;
2026-01-16 06:57:16 +00:00
type PathSegment = string ;
2026-02-20 05:09:17 +04:00
type ConfigSetParseOpts = {
strictJson? : boolean ;
} ;
2026-03-17 18:15:49 -05:00
type ConfigSetInputMode = ConfigSetDryRunInputMode ;
type ConfigSetOperation = {
inputMode : ConfigSetInputMode ;
requestedPath : PathSegment [ ] ;
setPath : PathSegment [ ] ;
value : unknown ;
touchedSecretTargetPath? : string ;
touchedProviderAlias? : string ;
assignedRef? : SecretRef ;
} ;
2026-01-16 06:57:16 +00:00
2026-02-27 23:35:57 -08:00
const OLLAMA_API_KEY_PATH : PathSegment [ ] = [ "models" , "providers" , "ollama" , "apiKey" ] ;
const OLLAMA_PROVIDER_PATH : PathSegment [ ] = [ "models" , "providers" , "ollama" ] ;
2026-03-19 16:05:43 -05:00
const GATEWAY_AUTH_MODE_PATH : PathSegment [ ] = [ "gateway" , "auth" , "mode" ] ;
2026-03-17 18:15:49 -05:00
const SECRET_PROVIDER_PATH_PREFIX : PathSegment [ ] = [ "secrets" , "providers" ] ;
const CONFIG_SET_EXAMPLE_VALUE = formatCliCommand (
"openclaw config set gateway.port 19001 --strict-json" ,
) ;
const CONFIG_SET_EXAMPLE_REF = formatCliCommand (
"openclaw config set channels.discord.token --ref-provider default --ref-source env --ref-id DISCORD_BOT_TOKEN" ,
) ;
const CONFIG_SET_EXAMPLE_PROVIDER = formatCliCommand (
"openclaw config set secrets.providers.vault --provider-source file --provider-path /etc/openclaw/secrets.json --provider-mode json" ,
) ;
const CONFIG_SET_EXAMPLE_BATCH = formatCliCommand (
"openclaw config set --batch-file ./config-set.batch.json --dry-run" ,
) ;
const CONFIG_SET_DESCRIPTION = [
"Set config values by path (value mode, ref/provider builder mode, or batch JSON mode)." ,
"Examples:" ,
CONFIG_SET_EXAMPLE_VALUE ,
CONFIG_SET_EXAMPLE_REF ,
CONFIG_SET_EXAMPLE_PROVIDER ,
CONFIG_SET_EXAMPLE_BATCH ,
] . join ( "\n" ) ;
class ConfigSetDryRunValidationError extends Error {
constructor ( readonly result : ConfigSetDryRunResult ) {
super ( "config set dry-run validation failed" ) ;
this . name = "ConfigSetDryRunValidationError" ;
}
}
2026-02-27 23:35:57 -08:00
2026-01-16 06:57:16 +00:00
function isIndexSegment ( raw : string ) : boolean {
return /^[0-9]+$/ . test ( raw ) ;
}
function parsePath ( raw : string ) : PathSegment [ ] {
const trimmed = raw . trim ( ) ;
2026-01-31 16:19:20 +09:00
if ( ! trimmed ) {
return [ ] ;
}
2026-01-16 06:57:16 +00:00
const parts : string [ ] = [ ] ;
let current = "" ;
let i = 0 ;
while ( i < trimmed . length ) {
const ch = trimmed [ i ] ;
if ( ch === "\\" ) {
const next = trimmed [ i + 1 ] ;
2026-01-31 16:19:20 +09:00
if ( next ) {
current += next ;
}
2026-01-16 06:57:16 +00:00
i += 2 ;
continue ;
}
if ( ch === "." ) {
2026-01-31 16:19:20 +09:00
if ( current ) {
parts . push ( current ) ;
}
2026-01-16 06:57:16 +00:00
current = "" ;
i += 1 ;
continue ;
}
if ( ch === "[" ) {
2026-01-31 16:19:20 +09:00
if ( current ) {
parts . push ( current ) ;
}
2026-01-16 06:57:16 +00:00
current = "" ;
const close = trimmed . indexOf ( "]" , i ) ;
2026-01-31 16:19:20 +09:00
if ( close === - 1 ) {
throw new Error ( ` Invalid path (missing "]"): ${ raw } ` ) ;
}
2026-01-16 06:57:16 +00:00
const inside = trimmed . slice ( i + 1 , close ) . trim ( ) ;
2026-01-31 16:19:20 +09:00
if ( ! inside ) {
throw new Error ( ` Invalid path (empty "[]"): ${ raw } ` ) ;
}
2026-01-16 06:57:16 +00:00
parts . push ( inside ) ;
i = close + 1 ;
continue ;
}
current += ch ;
i += 1 ;
}
2026-01-31 16:19:20 +09:00
if ( current ) {
parts . push ( current ) ;
}
2026-01-16 06:57:16 +00:00
return parts . map ( ( part ) = > part . trim ( ) ) . filter ( Boolean ) ;
}
2026-02-20 05:09:17 +04:00
function parseValue ( raw : string , opts : ConfigSetParseOpts ) : unknown {
2026-01-16 06:57:16 +00:00
const trimmed = raw . trim ( ) ;
2026-02-20 05:09:17 +04:00
if ( opts . strictJson ) {
2026-01-16 06:57:16 +00:00
try {
2026-03-20 10:10:57 -07:00
return JSON . parse ( trimmed ) ;
2026-01-16 06:57:16 +00:00
} catch ( err ) {
2026-03-20 10:10:57 -07:00
throw new Error ( ` Failed to parse JSON value: ${ String ( err ) } ` , { cause : err } ) ;
2026-01-16 06:57:16 +00:00
}
}
try {
return JSON5 . parse ( trimmed ) ;
} catch {
return raw ;
}
}
2026-02-22 23:51:13 -08:00
function hasOwnPathKey ( value : Record < string , unknown > , key : string ) : boolean {
return Object . prototype . hasOwnProperty . call ( value , key ) ;
}
2026-03-02 13:45:51 +08:00
function formatDoctorHint ( message : string ) : string {
return ` Run \` ${ formatCliCommand ( "openclaw doctor" ) } \` ${ message } ` ;
}
2026-02-22 23:51:13 -08:00
function validatePathSegments ( path : PathSegment [ ] ) : void {
for ( const segment of path ) {
if ( ! isIndexSegment ( segment ) && isBlockedObjectKey ( segment ) ) {
throw new Error ( ` Invalid path segment: ${ segment } ` ) ;
}
}
}
2026-01-16 06:57:16 +00:00
function getAtPath ( root : unknown , path : PathSegment [ ] ) : { found : boolean ; value? : unknown } {
let current : unknown = root ;
for ( const segment of path ) {
2026-01-31 16:19:20 +09:00
if ( ! current || typeof current !== "object" ) {
return { found : false } ;
}
2026-01-16 06:57:16 +00:00
if ( Array . isArray ( current ) ) {
2026-01-31 16:19:20 +09:00
if ( ! isIndexSegment ( segment ) ) {
return { found : false } ;
}
2026-01-16 06:57:16 +00:00
const index = Number . parseInt ( segment , 10 ) ;
if ( ! Number . isFinite ( index ) || index < 0 || index >= current . length ) {
return { found : false } ;
}
current = current [ index ] ;
continue ;
}
const record = current as Record < string , unknown > ;
2026-02-22 23:51:13 -08:00
if ( ! hasOwnPathKey ( record , segment ) ) {
2026-01-31 16:19:20 +09:00
return { found : false } ;
}
2026-01-16 06:57:16 +00:00
current = record [ segment ] ;
}
return { found : true , value : current } ;
}
function setAtPath ( root : Record < string , unknown > , path : PathSegment [ ] , value : unknown ) : void {
let current : unknown = root ;
for ( let i = 0 ; i < path . length - 1 ; i += 1 ) {
const segment = path [ i ] ;
const next = path [ i + 1 ] ;
const nextIsIndex = Boolean ( next && isIndexSegment ( next ) ) ;
if ( Array . isArray ( current ) ) {
if ( ! isIndexSegment ( segment ) ) {
throw new Error ( ` Expected numeric index for array segment " ${ segment } " ` ) ;
}
const index = Number . parseInt ( segment , 10 ) ;
const existing = current [ index ] ;
if ( ! existing || typeof existing !== "object" ) {
current [ index ] = nextIsIndex ? [ ] : { } ;
}
current = current [ index ] ;
continue ;
}
if ( ! current || typeof current !== "object" ) {
throw new Error ( ` Cannot traverse into " ${ segment } " (not an object) ` ) ;
}
const record = current as Record < string , unknown > ;
2026-02-22 23:51:13 -08:00
const existing = hasOwnPathKey ( record , segment ) ? record [ segment ] : undefined ;
2026-01-16 06:57:16 +00:00
if ( ! existing || typeof existing !== "object" ) {
record [ segment ] = nextIsIndex ? [ ] : { } ;
}
current = record [ segment ] ;
}
const last = path [ path . length - 1 ] ;
if ( Array . isArray ( current ) ) {
if ( ! isIndexSegment ( last ) ) {
throw new Error ( ` Expected numeric index for array segment " ${ last } " ` ) ;
}
const index = Number . parseInt ( last , 10 ) ;
current [ index ] = value ;
return ;
}
if ( ! current || typeof current !== "object" ) {
throw new Error ( ` Cannot set " ${ last } " (parent is not an object) ` ) ;
}
( current as Record < string , unknown > ) [ last ] = value ;
}
function unsetAtPath ( root : Record < string , unknown > , path : PathSegment [ ] ) : boolean {
let current : unknown = root ;
for ( let i = 0 ; i < path . length - 1 ; i += 1 ) {
const segment = path [ i ] ;
2026-01-31 16:19:20 +09:00
if ( ! current || typeof current !== "object" ) {
return false ;
}
2026-01-16 06:57:16 +00:00
if ( Array . isArray ( current ) ) {
2026-01-31 16:19:20 +09:00
if ( ! isIndexSegment ( segment ) ) {
return false ;
}
2026-01-16 06:57:16 +00:00
const index = Number . parseInt ( segment , 10 ) ;
2026-01-31 16:19:20 +09:00
if ( ! Number . isFinite ( index ) || index < 0 || index >= current . length ) {
return false ;
}
2026-01-16 06:57:16 +00:00
current = current [ index ] ;
continue ;
}
const record = current as Record < string , unknown > ;
2026-02-22 23:51:13 -08:00
if ( ! hasOwnPathKey ( record , segment ) ) {
2026-01-31 16:19:20 +09:00
return false ;
}
2026-01-16 06:57:16 +00:00
current = record [ segment ] ;
}
const last = path [ path . length - 1 ] ;
if ( Array . isArray ( current ) ) {
2026-01-31 16:19:20 +09:00
if ( ! isIndexSegment ( last ) ) {
return false ;
}
2026-01-16 06:57:16 +00:00
const index = Number . parseInt ( last , 10 ) ;
2026-01-31 16:19:20 +09:00
if ( ! Number . isFinite ( index ) || index < 0 || index >= current . length ) {
return false ;
}
2026-01-16 06:57:16 +00:00
current . splice ( index , 1 ) ;
return true ;
}
2026-01-31 16:19:20 +09:00
if ( ! current || typeof current !== "object" ) {
return false ;
}
2026-01-16 06:57:16 +00:00
const record = current as Record < string , unknown > ;
2026-02-22 23:51:13 -08:00
if ( ! hasOwnPathKey ( record , last ) ) {
2026-01-31 16:19:20 +09:00
return false ;
}
2026-01-16 06:57:16 +00:00
delete record [ last ] ;
return true ;
}
2026-02-14 00:27:30 +00:00
async function loadValidConfig ( runtime : RuntimeEnv = defaultRuntime ) {
2026-01-16 06:57:16 +00:00
const snapshot = await readConfigFileSnapshot ( ) ;
2026-01-31 16:19:20 +09:00
if ( snapshot . valid ) {
return snapshot ;
}
2026-02-14 00:27:30 +00:00
runtime . error ( ` Config invalid at ${ shortenHomePath ( snapshot . path ) } . ` ) ;
2026-03-02 20:05:12 -05:00
for ( const line of formatConfigIssueLines ( snapshot . issues , "-" , { normalizeRoot : true } ) ) {
2026-03-02 13:45:51 +08:00
runtime . error ( line ) ;
2026-01-16 06:57:16 +00:00
}
2026-03-02 13:45:51 +08:00
runtime . error ( formatDoctorHint ( "to repair, then retry." ) ) ;
2026-02-14 00:27:30 +00:00
runtime . exit ( 1 ) ;
2026-01-16 06:57:16 +00:00
return snapshot ;
}
2026-02-14 00:27:30 +00:00
function parseRequiredPath ( path : string ) : PathSegment [ ] {
const parsedPath = parsePath ( path ) ;
if ( parsedPath . length === 0 ) {
throw new Error ( "Path is empty." ) ;
}
2026-02-22 23:51:13 -08:00
validatePathSegments ( parsedPath ) ;
2026-02-14 00:27:30 +00:00
return parsedPath ;
}
2026-02-27 23:35:57 -08:00
function pathEquals ( path : PathSegment [ ] , expected : PathSegment [ ] ) : boolean {
return (
path . length === expected . length && path . every ( ( segment , index ) = > segment === expected [ index ] )
) ;
}
function ensureValidOllamaProviderForApiKeySet (
root : Record < string , unknown > ,
path : PathSegment [ ] ,
) : void {
if ( ! pathEquals ( path , OLLAMA_API_KEY_PATH ) ) {
return ;
}
const existing = getAtPath ( root , OLLAMA_PROVIDER_PATH ) ;
if ( existing . found ) {
return ;
}
setAtPath ( root , OLLAMA_PROVIDER_PATH , {
baseUrl : OLLAMA_DEFAULT_BASE_URL ,
api : "ollama" ,
models : [ ] ,
} ) ;
}
2026-03-19 16:05:43 -05:00
function pruneInactiveGatewayAuthCredentials ( params : {
root : Record < string , unknown > ;
operations : ConfigSetOperation [ ] ;
} ) : string [ ] {
const touchedGatewayAuthMode = params . operations . some ( ( operation ) = >
pathEquals ( operation . requestedPath , GATEWAY_AUTH_MODE_PATH ) ,
) ;
if ( ! touchedGatewayAuthMode ) {
return [ ] ;
}
const gatewayRaw = params . root . gateway ;
if ( ! gatewayRaw || typeof gatewayRaw !== "object" || Array . isArray ( gatewayRaw ) ) {
return [ ] ;
}
const gateway = gatewayRaw as Record < string , unknown > ;
const authRaw = gateway . auth ;
if ( ! authRaw || typeof authRaw !== "object" || Array . isArray ( authRaw ) ) {
return [ ] ;
}
const auth = authRaw as Record < string , unknown > ;
const mode = typeof auth . mode === "string" ? auth . mode . trim ( ) : "" ;
const removedPaths : string [ ] = [ ] ;
const remove = ( key : "token" | "password" ) = > {
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 ;
}
2026-03-17 18:15:49 -05:00
function toDotPath ( path : PathSegment [ ] ) : string {
return path . join ( "." ) ;
}
function parseSecretRefSource ( raw : string , label : string ) : SecretRefSource {
const source = raw . trim ( ) ;
if ( source === "env" || source === "file" || source === "exec" ) {
return source ;
}
throw new Error ( ` ${ label } must be one of: env, file, exec. ` ) ;
}
function parseSecretRefBuilder ( params : {
provider : string ;
source : string ;
id : string ;
fieldPrefix : string ;
} ) : SecretRef {
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 === "file" && ! isValidFileSecretRefId ( id ) ) {
throw new Error (
` ${ params . fieldPrefix } .id must be an absolute JSON pointer (or "value" for singleValue mode). ` ,
) ;
}
if ( source === "exec" ) {
const validated = validateExecSecretRefId ( id ) ;
if ( ! validated . ok ) {
throw new Error ( formatExecSecretRefIdValidationMessage ( ) ) ;
}
}
return { source , provider , id } ;
}
function parseOptionalPositiveInteger ( raw : string | undefined , flag : string ) : number | undefined {
if ( raw === undefined ) {
return undefined ;
}
const trimmed = raw . trim ( ) ;
if ( ! trimmed ) {
throw new Error ( ` ${ flag } must not be empty. ` ) ;
}
const parsed = Number ( trimmed ) ;
if ( ! Number . isInteger ( parsed ) || parsed <= 0 ) {
throw new Error ( ` ${ flag } must be a positive integer. ` ) ;
}
return parsed ;
}
function parseProviderEnvEntries (
entries : string [ ] | undefined ,
) : Record < string , string > | undefined {
if ( ! entries || entries . length === 0 ) {
return undefined ;
}
const env : Record < string , string > = { } ;
for ( const entry of entries ) {
const separator = entry . indexOf ( "=" ) ;
if ( separator <= 0 ) {
throw new Error ( ` --provider-env expects KEY=VALUE entries (received: " ${ entry } "). ` ) ;
}
const key = entry . slice ( 0 , separator ) . trim ( ) ;
if ( ! key ) {
throw new Error ( ` --provider-env key must not be empty (received: " ${ entry } "). ` ) ;
}
env [ key ] = entry . slice ( separator + 1 ) ;
}
return Object . keys ( env ) . length > 0 ? env : undefined ;
}
function parseProviderAliasPath ( path : PathSegment [ ] ) : string {
const expectedPrefixMatches =
path . length === 3 &&
path [ 0 ] === SECRET_PROVIDER_PATH_PREFIX [ 0 ] &&
path [ 1 ] === SECRET_PROVIDER_PATH_PREFIX [ 1 ] ;
if ( ! expectedPrefixMatches ) {
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"). ` ,
) ;
}
return alias ;
}
function buildProviderFromBuilder ( opts : ConfigSetOptions ) : SecretProviderConfig {
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 : SecretProviderConfig ;
if ( source === "env" ) {
const allowlist = ( opts . providerAllowlist ? ? [ ] ) . map ( ( entry ) = > entry . trim ( ) ) . filter ( Boolean ) ;
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 : undefined ;
provider = {
source : "file" ,
path : filePath ,
. . . ( mode ? { mode } : { } ) ,
. . . ( timeoutMs !== undefined ? { timeoutMs } : { } ) ,
. . . ( maxBytes !== undefined ? { maxBytes } : { } ) ,
} ;
} 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 && opts . providerArg . length > 0
? { args : opts.providerArg.map ( ( entry ) = > entry . trim ( ) ) }
: { } ) ,
. . . ( timeoutMs !== undefined ? { timeoutMs } : { } ) ,
. . . ( noOutputTimeoutMs !== undefined ? { noOutputTimeoutMs } : { } ) ,
. . . ( maxOutputBytes !== undefined ? { maxOutputBytes } : { } ) ,
. . . ( opts . providerJsonOnly ? { jsonOnly : true } : { } ) ,
. . . ( providerEnv ? { env : providerEnv } : { } ) ,
. . . ( opts . providerPassEnv && opts . providerPassEnv . length > 0
? { passEnv : opts.providerPassEnv.map ( ( entry ) = > entry . trim ( ) ) . filter ( Boolean ) }
: { } ) ,
. . . ( opts . providerTrustedDir && opts . providerTrustedDir . length > 0
? { trustedDirs : opts.providerTrustedDir.map ( ( entry ) = > entry . trim ( ) ) . filter ( Boolean ) }
: { } ) ,
. . . ( opts . providerAllowInsecurePath ? { allowInsecurePath : true } : { } ) ,
. . . ( opts . providerAllowSymlinkCommand ? { allowSymlinkCommand : true } : { } ) ,
} ;
}
const validated = SecretProviderSchema . safeParse ( provider ) ;
if ( ! validated . success ) {
const issue = validated . error . issues [ 0 ] ;
const issuePath = issue ? . path ? . join ( "." ) ? ? "<provider>" ;
const issueMessage = issue ? . message ? ? "Invalid provider config." ;
throw new Error ( ` Provider builder config invalid at ${ issuePath } : ${ issueMessage } ` ) ;
}
return validated . data ;
}
function parseSecretRefFromUnknown ( value : unknown , label : string ) : SecretRef {
if ( ! value || typeof value !== "object" || Array . isArray ( value ) ) {
throw new Error ( ` ${ label } must be an object with source/provider/id. ` ) ;
}
const candidate = value as Record < string , unknown > ;
if (
typeof candidate . provider !== "string" ||
typeof candidate . source !== "string" ||
typeof candidate . id !== "string"
) {
throw new Error ( ` ${ label } must include string fields: source, provider, id. ` ) ;
}
return parseSecretRefBuilder ( {
provider : candidate.provider ,
source : candidate.source ,
id : candidate.id ,
fieldPrefix : label ,
} ) ;
}
function buildRefAssignmentOperation ( params : {
requestedPath : PathSegment [ ] ;
ref : SecretRef ;
inputMode : ConfigSetInputMode ;
} ) : ConfigSetOperation {
const resolved = resolveConfigSecretTargetByPath ( params . requestedPath ) ;
if ( resolved ? . entry . secretShape === "sibling_ref" && resolved . refPathSegments ) {
return {
inputMode : params.inputMode ,
requestedPath : params.requestedPath ,
setPath : resolved.refPathSegments ,
value : params.ref ,
touchedSecretTargetPath : toDotPath ( resolved . pathSegments ) ,
assignedRef : params.ref ,
. . . ( resolved . providerId ? { touchedProviderAlias : resolved.providerId } : { } ) ,
} ;
}
return {
inputMode : params.inputMode ,
requestedPath : params.requestedPath ,
setPath : params.requestedPath ,
value : params.ref ,
touchedSecretTargetPath : resolved
? toDotPath ( resolved . pathSegments )
: toDotPath ( params . requestedPath ) ,
assignedRef : params.ref ,
. . . ( resolved ? . providerId ? { touchedProviderAlias : resolved.providerId } : { } ) ,
} ;
}
function parseProviderAliasFromTargetPath ( path : PathSegment [ ] ) : string | null {
if (
2026-03-17 18:28:46 -05:00
path . length >= 3 &&
2026-03-17 18:15:49 -05:00
path [ 0 ] === SECRET_PROVIDER_PATH_PREFIX [ 0 ] &&
path [ 1 ] === SECRET_PROVIDER_PATH_PREFIX [ 1 ]
) {
return path [ 2 ] ? ? null ;
}
return null ;
}
function buildValueAssignmentOperation ( params : {
requestedPath : PathSegment [ ] ;
value : unknown ;
inputMode : ConfigSetInputMode ;
} ) : ConfigSetOperation {
const resolved = resolveConfigSecretTargetByPath ( params . requestedPath ) ;
const providerAlias = parseProviderAliasFromTargetPath ( params . requestedPath ) ;
const coercedRef = coerceSecretRef ( params . value ) ;
return {
inputMode : params.inputMode ,
requestedPath : params.requestedPath ,
setPath : params.requestedPath ,
value : params.value ,
. . . ( resolved ? { touchedSecretTargetPath : toDotPath ( resolved . pathSegments ) } : { } ) ,
. . . ( providerAlias ? { touchedProviderAlias : providerAlias } : { } ) ,
. . . ( coercedRef ? { assignedRef : coercedRef } : { } ) ,
} ;
}
function parseBatchOperations ( entries : ConfigSetBatchEntry [ ] ) : ConfigSetOperation [ ] {
const operations : ConfigSetOperation [ ] = [ ] ;
for ( const [ index , entry ] of entries . entries ( ) ) {
const path = parseRequiredPath ( entry . path ) ;
if ( entry . ref !== undefined ) {
const ref = parseSecretRefFromUnknown ( entry . ref , ` batch[ ${ index } ].ref ` ) ;
operations . push (
buildRefAssignmentOperation ( {
requestedPath : path ,
ref ,
inputMode : "json" ,
} ) ,
) ;
continue ;
}
if ( entry . provider !== undefined ) {
const alias = parseProviderAliasPath ( path ) ;
const validated = SecretProviderSchema . safeParse ( entry . provider ) ;
if ( ! validated . success ) {
const issue = validated . error . issues [ 0 ] ;
const issuePath = issue ? . path ? . join ( "." ) ? ? "<provider>" ;
throw new Error (
` batch[ ${ index } ].provider invalid at ${ issuePath } : ${ issue ? . message ? ? "" } ` ,
) ;
}
operations . push ( {
inputMode : "json" ,
requestedPath : path ,
setPath : path ,
value : validated.data ,
touchedProviderAlias : alias ,
} ) ;
continue ;
}
operations . push (
buildValueAssignmentOperation ( {
requestedPath : path ,
value : entry.value ,
inputMode : "json" ,
} ) ,
) ;
}
return operations ;
}
function modeError ( message : string ) : Error {
return new Error ( ` config set mode error: ${ message } ` ) ;
}
function buildSingleSetOperations ( params : {
path? : string ;
value? : string ;
opts : ConfigSetOptions ;
} ) : ConfigSetOperation [ ] {
const pathProvided = typeof params . path === "string" && params . path . trim ( ) . length > 0 ;
const parsedPath = pathProvided ? parseRequiredPath ( params . path as string ) : null ;
const strictJson = Boolean ( params . opts . strictJson || params . opts . json ) ;
const modeResolution = resolveConfigSetMode ( {
hasBatchMode : false ,
hasRefBuilderOptions : hasRefBuilderOptions ( params . opts ) ,
hasProviderBuilderOptions : hasProviderBuilderOptions ( params . opts ) ,
strictJson ,
} ) ;
if ( ! modeResolution . ok ) {
throw modeError ( modeResolution . error ) ;
}
if ( modeResolution . mode === "ref_builder" ) {
if ( ! pathProvided || ! parsedPath ) {
throw modeError ( "ref builder mode requires <path>." ) ;
}
if ( params . value !== undefined ) {
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>, and --ref-id <id>." ,
) ;
}
const ref = parseSecretRefBuilder ( {
provider : params.opts.refProvider ,
source : params.opts.refSource ,
id : params.opts.refId ,
fieldPrefix : "ref" ,
} ) ;
return [
buildRefAssignmentOperation ( {
requestedPath : parsedPath ,
ref ,
inputMode : "builder" ,
} ) ,
] ;
}
if ( modeResolution . mode === "provider_builder" ) {
if ( ! pathProvided || ! parsedPath ) {
throw modeError ( "provider builder mode requires <path>." ) ;
}
if ( params . value !== undefined ) {
throw modeError ( "provider builder mode does not accept <value>." ) ;
}
const alias = parseProviderAliasPath ( parsedPath ) ;
const provider = buildProviderFromBuilder ( params . opts ) ;
return [
{
inputMode : "builder" ,
requestedPath : parsedPath ,
setPath : parsedPath ,
value : provider ,
touchedProviderAlias : alias ,
} ,
] ;
}
if ( ! pathProvided || ! parsedPath ) {
throw modeError ( "value/json mode requires <path> when batch mode is not used." ) ;
}
if ( params . value === undefined ) {
throw modeError ( "value/json mode requires <value>." ) ;
}
const parsedValue = parseValue ( params . value , { strictJson } ) ;
return [
buildValueAssignmentOperation ( {
requestedPath : parsedPath ,
value : parsedValue ,
inputMode : modeResolution.mode === "json" ? "json" : "value" ,
} ) ,
] ;
}
function collectDryRunRefs ( params : {
config : OpenClawConfig ;
operations : ConfigSetOperation [ ] ;
} ) : SecretRef [ ] {
const refsByKey = new Map < string , SecretRef > ( ) ;
const targetPaths = new Set < string > ( ) ;
const providerAliases = new Set < string > ( ) ;
for ( const operation of params . operations ) {
if ( operation . assignedRef ) {
refsByKey . set ( secretRefKey ( operation . assignedRef ) , operation . assignedRef ) ;
}
if ( operation . touchedSecretTargetPath ) {
targetPaths . add ( operation . touchedSecretTargetPath ) ;
}
if ( operation . touchedProviderAlias ) {
providerAliases . add ( operation . touchedProviderAlias ) ;
}
}
if ( targetPaths . size === 0 && providerAliases . size === 0 ) {
return [ . . . refsByKey . values ( ) ] ;
}
const defaults = params . config . secrets ? . defaults ;
for ( const target of discoverConfigSecretTargets ( params . config ) ) {
const { ref } = resolveSecretInputRef ( {
value : target.value ,
refValue : target.refValue ,
defaults ,
} ) ;
if ( ! ref ) {
continue ;
}
if ( targetPaths . has ( target . path ) || providerAliases . has ( ref . provider ) ) {
refsByKey . set ( secretRefKey ( ref ) , ref ) ;
}
}
return [ . . . refsByKey . values ( ) ] ;
}
async function collectDryRunResolvabilityErrors ( params : {
refs : SecretRef [ ] ;
config : OpenClawConfig ;
} ) : Promise < ConfigSetDryRunError [ ] > {
const failures : ConfigSetDryRunError [ ] = [ ] ;
for ( const ref of params . refs ) {
try {
await resolveSecretRefValue ( ref , {
config : params.config ,
env : process.env ,
} ) ;
} catch ( err ) {
failures . push ( {
kind : "resolvability" ,
message : String ( err ) ,
ref : ` ${ ref . source } : ${ ref . provider } : ${ ref . id } ` ,
} ) ;
}
}
return failures ;
}
2026-03-17 20:20:11 -05:00
function collectDryRunStaticErrorsForSkippedExecRefs ( params : {
refs : SecretRef [ ] ;
config : OpenClawConfig ;
} ) : ConfigSetDryRunError [ ] {
const failures : ConfigSetDryRunError [ ] = [ ] ;
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 : { refs : SecretRef [ ] ; allowExecInDryRun : boolean } ) : {
refsToResolve : SecretRef [ ] ;
skippedExecRefs : SecretRef [ ] ;
} {
const refsToResolve : SecretRef [ ] = [ ] ;
const skippedExecRefs : SecretRef [ ] = [ ] ;
for ( const ref of params . refs ) {
if ( ref . source === "exec" && ! params . allowExecInDryRun ) {
skippedExecRefs . push ( ref ) ;
continue ;
}
refsToResolve . push ( ref ) ;
}
return { refsToResolve , skippedExecRefs } ;
}
2026-03-17 18:15:49 -05:00
function collectDryRunSchemaErrors ( config : OpenClawConfig ) : ConfigSetDryRunError [ ] {
const validated = validateConfigObjectRaw ( config ) ;
if ( validated . ok ) {
return [ ] ;
}
return formatConfigIssueLines ( validated . issues , "-" , { normalizeRoot : true } ) . map ( ( message ) = > ( {
kind : "schema" ,
message ,
} ) ) ;
}
2026-03-17 20:20:11 -05:00
function formatDryRunFailureMessage ( params : {
errors : ConfigSetDryRunError [ ] ;
skippedExecRefs : number ;
} ) : string {
const { errors , skippedExecRefs } = params ;
2026-03-17 18:15:49 -05:00
const schemaErrors = errors . filter ( ( error ) = > error . kind === "schema" ) ;
const resolveErrors = errors . filter ( ( error ) = > error . kind === "resolvability" ) ;
const lines : string [ ] = [ ] ;
if ( schemaErrors . length > 0 ) {
lines . push ( "Dry run failed: config schema validation failed." ) ;
lines . push ( . . . schemaErrors . map ( ( error ) = > ` - ${ error . message } ` ) ) ;
}
if ( resolveErrors . length > 0 ) {
lines . push (
` Dry run failed: ${ resolveErrors . length } SecretRef assignment(s) could not be resolved. ` ,
) ;
lines . push (
. . . resolveErrors
. slice ( 0 , 5 )
. map ( ( error ) = > ` - ${ error . ref ? ? "<unknown-ref>" } -> ${ error . message } ` ) ,
) ;
if ( resolveErrors . length > 5 ) {
lines . push ( ` - ... ${ resolveErrors . length - 5 } more ` ) ;
}
}
2026-03-17 20:20:11 -05:00
if ( skippedExecRefs > 0 ) {
lines . push (
` Dry run note: skipped ${ skippedExecRefs } exec SecretRef resolvability check(s). Re-run with --allow-exec to execute exec providers during dry-run. ` ,
) ;
}
2026-03-17 18:15:49 -05:00
return lines . join ( "\n" ) ;
}
export async function runConfigSet ( opts : {
path? : string ;
value? : string ;
cliOptions : ConfigSetOptions ;
runtime? : RuntimeEnv ;
} ) {
const runtime = opts . runtime ? ? defaultRuntime ;
try {
2026-03-17 18:28:46 -05:00
const isBatchMode = hasBatchMode ( opts . cliOptions ) ;
2026-03-17 18:15:49 -05:00
const modeResolution = resolveConfigSetMode ( {
2026-03-17 18:28:46 -05:00
hasBatchMode : isBatchMode ,
2026-03-17 18:15:49 -05:00
hasRefBuilderOptions : hasRefBuilderOptions ( opts . cliOptions ) ,
hasProviderBuilderOptions : hasProviderBuilderOptions ( opts . cliOptions ) ,
strictJson : Boolean ( opts . cliOptions . strictJson || opts . cliOptions . json ) ,
} ) ;
if ( ! modeResolution . ok ) {
throw modeError ( modeResolution . error ) ;
}
2026-03-17 20:20:11 -05:00
if ( opts . cliOptions . allowExec && ! opts . cliOptions . dryRun ) {
throw modeError ( "--allow-exec requires --dry-run." ) ;
}
2026-03-17 18:15:49 -05:00
const batchEntries = parseBatchSource ( opts . cliOptions ) ;
if ( batchEntries ) {
if ( opts . path !== undefined || opts . value !== undefined ) {
throw modeError ( "batch mode does not accept <path> or <value> arguments." ) ;
}
}
const operations = batchEntries
? parseBatchOperations ( batchEntries )
: buildSingleSetOperations ( {
path : opts.path ,
value : opts.value ,
opts : opts.cliOptions ,
} ) ;
const snapshot = await loadValidConfig ( runtime ) ;
// Use snapshot.resolved (config after $include and ${ENV} resolution, but BEFORE runtime defaults)
// instead of snapshot.config (runtime-merged with defaults).
// This prevents runtime defaults from leaking into the written config file (issue #6070)
const next = structuredClone ( snapshot . resolved ) as Record < string , unknown > ;
for ( const operation of operations ) {
ensureValidOllamaProviderForApiKeySet ( next , operation . setPath ) ;
setAtPath ( next , operation . setPath , operation . value ) ;
}
2026-03-19 16:05:43 -05:00
const removedGatewayAuthPaths = pruneInactiveGatewayAuthCredentials ( {
root : next ,
operations ,
} ) ;
2026-03-17 18:15:49 -05:00
const nextConfig = next as OpenClawConfig ;
if ( opts . cliOptions . dryRun ) {
const hasJsonMode = operations . some ( ( operation ) = > operation . inputMode === "json" ) ;
const hasBuilderMode = operations . some ( ( operation ) = > operation . inputMode === "builder" ) ;
const refs =
hasJsonMode || hasBuilderMode
? collectDryRunRefs ( {
config : nextConfig ,
operations ,
} )
: [ ] ;
2026-03-17 20:20:11 -05:00
const selectedDryRunRefs = selectDryRunRefsForResolution ( {
refs ,
allowExecInDryRun : Boolean ( opts . cliOptions . allowExec ) ,
} ) ;
2026-03-17 18:15:49 -05:00
const errors : ConfigSetDryRunError [ ] = [ ] ;
if ( hasJsonMode ) {
errors . push ( . . . collectDryRunSchemaErrors ( nextConfig ) ) ;
}
if ( hasJsonMode || hasBuilderMode ) {
2026-03-17 20:20:11 -05:00
errors . push (
. . . collectDryRunStaticErrorsForSkippedExecRefs ( {
refs : selectedDryRunRefs.skippedExecRefs ,
config : nextConfig ,
} ) ,
) ;
2026-03-17 18:15:49 -05:00
errors . push (
. . . ( await collectDryRunResolvabilityErrors ( {
2026-03-17 20:20:11 -05:00
refs : selectedDryRunRefs.refsToResolve ,
2026-03-17 18:15:49 -05:00
config : nextConfig ,
} ) ) ,
) ;
}
const dryRunResult : ConfigSetDryRunResult = {
ok : errors.length === 0 ,
operations : operations.length ,
configPath : shortenHomePath ( snapshot . path ) ,
inputModes : [ . . . new Set ( operations . map ( ( operation ) = > operation . inputMode ) ) ] ,
checks : {
schema : hasJsonMode ,
resolvability : hasJsonMode || hasBuilderMode ,
2026-03-17 20:20:11 -05:00
resolvabilityComplete :
( hasJsonMode || hasBuilderMode ) && selectedDryRunRefs . skippedExecRefs . length === 0 ,
2026-03-17 18:15:49 -05:00
} ,
2026-03-17 20:20:11 -05:00
refsChecked : selectedDryRunRefs.refsToResolve.length ,
skippedExecRefs : selectedDryRunRefs.skippedExecRefs.length ,
2026-03-17 18:15:49 -05:00
. . . ( errors . length > 0 ? { errors } : { } ) ,
} ;
if ( errors . length > 0 ) {
if ( opts . cliOptions . json ) {
throw new ConfigSetDryRunValidationError ( dryRunResult ) ;
}
2026-03-17 20:20:11 -05:00
throw new Error (
formatDryRunFailureMessage ( {
errors ,
skippedExecRefs : selectedDryRunRefs.skippedExecRefs.length ,
} ) ,
) ;
2026-03-17 18:15:49 -05:00
}
if ( opts . cliOptions . json ) {
runtime . log ( JSON . stringify ( dryRunResult , null , 2 ) ) ;
} else {
2026-03-17 18:28:46 -05:00
if ( ! dryRunResult . checks . schema && ! dryRunResult . 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." ,
) ,
) ;
}
2026-03-17 20:20:11 -05:00
if ( dryRunResult . skippedExecRefs > 0 ) {
runtime . log (
info (
` Dry run note: skipped ${ dryRunResult . skippedExecRefs } exec SecretRef resolvability check(s). Re-run with --allow-exec to execute exec providers during dry-run. ` ,
) ,
) ;
}
2026-03-17 18:15:49 -05:00
runtime . log (
info (
` Dry run successful: ${ operations . length } update(s) validated against ${ shortenHomePath ( snapshot . path ) } . ` ,
) ,
) ;
}
return ;
}
await writeConfigFile ( next ) ;
2026-03-19 16:05:43 -05:00
if ( removedGatewayAuthPaths . length > 0 ) {
runtime . log (
info (
` Removed inactive ${ removedGatewayAuthPaths . join ( ", " ) } for gateway.auth.mode= ${ String ( nextConfig . gateway ? . auth ? . mode ? ? "<unset>" ) } . ` ,
) ,
) ;
}
2026-03-17 18:15:49 -05:00
if ( operations . length === 1 ) {
runtime . log (
info (
` Updated ${ toDotPath ( operations [ 0 ] ? . requestedPath ? ? [ ] ) } . Restart the gateway to apply. ` ,
) ,
) ;
return ;
}
runtime . log ( info ( ` Updated ${ operations . length } config paths. Restart the gateway to apply. ` ) ) ;
} catch ( err ) {
if (
opts . cliOptions . dryRun &&
opts . cliOptions . json &&
err instanceof ConfigSetDryRunValidationError
) {
runtime . log ( JSON . stringify ( err . result , null , 2 ) ) ;
runtime . exit ( 1 ) ;
return ;
}
runtime . error ( danger ( String ( err ) ) ) ;
runtime . exit ( 1 ) ;
}
}
2026-02-14 00:27:30 +00:00
export async function runConfigGet ( opts : { path : string ; json? : boolean ; runtime? : RuntimeEnv } ) {
const runtime = opts . runtime ? ? defaultRuntime ;
try {
const parsedPath = parseRequiredPath ( opts . path ) ;
const snapshot = await loadValidConfig ( runtime ) ;
2026-02-22 19:37:33 -05:00
const redacted = redactConfigObject ( snapshot . config ) ;
const res = getAtPath ( redacted , parsedPath ) ;
2026-02-14 00:27:30 +00:00
if ( ! res . found ) {
runtime . error ( danger ( ` Config path not found: ${ opts . path } ` ) ) ;
runtime . exit ( 1 ) ;
return ;
}
if ( opts . json ) {
runtime . log ( JSON . stringify ( res . value ? ? null , null , 2 ) ) ;
return ;
}
if (
typeof res . value === "string" ||
typeof res . value === "number" ||
typeof res . value === "boolean"
) {
runtime . log ( String ( res . value ) ) ;
return ;
}
runtime . log ( JSON . stringify ( res . value ? ? null , null , 2 ) ) ;
} catch ( err ) {
runtime . error ( danger ( String ( err ) ) ) ;
runtime . exit ( 1 ) ;
}
}
export async function runConfigUnset ( opts : { path : string ; runtime? : RuntimeEnv } ) {
const runtime = opts . runtime ? ? defaultRuntime ;
try {
const parsedPath = parseRequiredPath ( opts . path ) ;
const snapshot = await loadValidConfig ( runtime ) ;
// Use snapshot.resolved (config after $include and ${ENV} resolution, but BEFORE runtime defaults)
// instead of snapshot.config (runtime-merged with defaults).
// This prevents runtime defaults from leaking into the written config file (issue #6070)
const next = structuredClone ( snapshot . resolved ) as Record < string , unknown > ;
const removed = unsetAtPath ( next , parsedPath ) ;
if ( ! removed ) {
runtime . error ( danger ( ` Config path not found: ${ opts . path } ` ) ) ;
runtime . exit ( 1 ) ;
return ;
}
2026-02-21 21:07:50 -08:00
await writeConfigFile ( next , { unsetPaths : [ parsedPath ] } ) ;
2026-02-14 00:27:30 +00:00
runtime . log ( info ( ` Removed ${ opts . path } . Restart the gateway to apply. ` ) ) ;
} catch ( err ) {
runtime . error ( danger ( String ( err ) ) ) ;
runtime . exit ( 1 ) ;
}
}
2026-03-02 12:33:20 +08:00
export async function runConfigFile ( opts : { runtime? : RuntimeEnv } ) {
const runtime = opts . runtime ? ? defaultRuntime ;
try {
const snapshot = await readConfigFileSnapshot ( ) ;
runtime . log ( shortenHomePath ( snapshot . path ) ) ;
} catch ( err ) {
runtime . error ( danger ( String ( err ) ) ) ;
runtime . exit ( 1 ) ;
}
}
2026-03-02 13:45:51 +08:00
export async function runConfigValidate ( opts : { json? : boolean ; runtime? : RuntimeEnv } = { } ) {
const runtime = opts . runtime ? ? defaultRuntime ;
let outputPath = CONFIG_PATH ? ? "openclaw.json" ;
try {
const snapshot = await readConfigFileSnapshot ( ) ;
outputPath = snapshot . path ;
const shortPath = shortenHomePath ( outputPath ) ;
if ( ! snapshot . exists ) {
if ( opts . json ) {
runtime . log ( JSON . stringify ( { valid : false , path : outputPath , error : "file not found" } ) ) ;
} else {
runtime . error ( danger ( ` Config file not found: ${ shortPath } ` ) ) ;
}
runtime . exit ( 1 ) ;
return ;
}
if ( ! snapshot . valid ) {
const issues = normalizeConfigIssues ( snapshot . issues ) ;
if ( opts . json ) {
runtime . log ( JSON . stringify ( { valid : false , path : outputPath , issues } , null , 2 ) ) ;
} else {
runtime . error ( danger ( ` Config invalid at ${ shortPath } : ` ) ) ;
2026-03-02 20:05:12 -05:00
for ( const line of formatConfigIssueLines ( issues , danger ( "× " ) , { normalizeRoot : true } ) ) {
2026-03-02 13:45:51 +08:00
runtime . error ( ` ${ line } ` ) ;
}
runtime . error ( "" ) ;
runtime . error ( formatDoctorHint ( "to repair, or fix the keys above manually." ) ) ;
}
runtime . exit ( 1 ) ;
return ;
}
if ( opts . json ) {
runtime . log ( JSON . stringify ( { valid : true , path : outputPath } ) ) ;
} else {
runtime . log ( success ( ` Config valid: ${ shortPath } ` ) ) ;
}
} catch ( err ) {
if ( opts . json ) {
runtime . log ( JSON . stringify ( { valid : false , path : outputPath , error : String ( err ) } ) ) ;
} else {
runtime . error ( danger ( ` Config validation error: ${ String ( err ) } ` ) ) ;
}
runtime . exit ( 1 ) ;
}
}
2026-01-16 06:57:16 +00:00
export function registerConfigCli ( program : Command ) {
const cmd = program
. command ( "config" )
2026-02-16 22:06:25 +01:00
. description (
2026-03-16 19:50:31 -05:00
"Non-interactive config helpers (get/set/unset/file/validate). Run without subcommand for guided setup." ,
2026-02-16 22:06:25 +01:00
)
2026-01-16 06:57:16 +00:00
. addHelpText (
"after" ,
( ) = >
2026-01-30 03:15:10 +01:00
` \ n ${ theme . muted ( "Docs:" ) } ${ formatDocsLink ( "/cli/config" , "docs.openclaw.ai/cli/config" ) } \ n ` ,
2026-01-16 06:57:16 +00:00
)
. option (
"--section <section>" ,
2026-03-16 19:50:31 -05:00
"Configuration sections for guided setup (repeatable). Use with no subcommand." ,
2026-01-16 06:57:16 +00:00
( value : string , previous : string [ ] ) = > [ . . . previous , value ] ,
[ ] as string [ ] ,
)
. action ( async ( opts ) = > {
2026-02-15 14:20:06 +00:00
const { configureCommandFromSectionsArg } = await import ( "../commands/configure.js" ) ;
await configureCommandFromSectionsArg ( opts . section , defaultRuntime ) ;
2026-01-16 06:57:16 +00:00
} ) ;
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 : string , opts ) = > {
2026-02-14 00:27:30 +00:00
await runConfigGet ( { path , json : Boolean ( opts . json ) } ) ;
2026-01-16 06:57:16 +00:00
} ) ;
cmd
. command ( "set" )
2026-03-17 18:15:49 -05:00
. description ( CONFIG_SET_DESCRIPTION )
. argument ( "[path]" , "Config path (dot or bracket notation)" )
2026-03-20 10:10:57 -07:00
. argument ( "[value]" , "Value (JSON/JSON5 or raw string)" )
. option ( "--strict-json" , "Strict JSON parsing (error instead of raw string fallback)" , false )
2026-02-20 05:09:17 +04:00
. option ( "--json" , "Legacy alias for --strict-json" , false )
2026-03-17 18:28:46 -05:00
. option (
"--dry-run" ,
2026-03-17 20:20:11 -05:00
"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)" ,
2026-03-17 18:28:46 -05:00
false ,
)
2026-03-17 18:15:49 -05:00
. option ( "--ref-provider <alias>" , "SecretRef builder: provider alias" )
. option ( "--ref-source <source>" , "SecretRef builder: source (env|file|exec)" )
. option ( "--ref-id <id>" , "SecretRef builder: ref id" )
. option ( "--provider-source <source>" , "Provider builder: source (env|file|exec)" )
. option (
"--provider-allowlist <envVar>" ,
"Provider builder (env): allowlist entry (repeatable)" ,
( value : string , previous : string [ ] ) = > [ . . . previous , value ] ,
[ ] as string [ ] ,
)
. 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)" ,
( value : string , previous : string [ ] ) = > [ . . . previous , value ] ,
[ ] as string [ ] ,
)
. 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)" ,
( value : string , previous : string [ ] ) = > [ . . . previous , value ] ,
[ ] as string [ ] ,
)
. option (
"--provider-pass-env <envVar>" ,
"Provider builder (exec): pass host env var (repeatable)" ,
( value : string , previous : string [ ] ) = > [ . . . previous , value ] ,
[ ] as string [ ] ,
)
. option (
"--provider-trusted-dir <path>" ,
"Provider builder (exec): trusted directory (repeatable)" ,
( value : string , previous : string [ ] ) = > [ . . . previous , value ] ,
[ ] as string [ ] ,
)
. option (
"--provider-allow-insecure-path" ,
"Provider builder (exec): bypass strict path permission checks" ,
false ,
)
. option (
"--provider-allow-symlink-command" ,
"Provider builder (exec): allow command symlink path" ,
false ,
)
. 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 : string | undefined , value : string | undefined , opts : ConfigSetOptions ) = > {
await runConfigSet ( {
path ,
value ,
cliOptions : opts ,
} ) ;
2026-01-16 06:57:16 +00:00
} ) ;
cmd
. command ( "unset" )
. description ( "Remove a config value by dot path" )
. argument ( "<path>" , "Config path (dot or bracket notation)" )
. action ( async ( path : string ) = > {
2026-02-14 00:27:30 +00:00
await runConfigUnset ( { path } ) ;
2026-01-16 06:57:16 +00:00
} ) ;
2026-03-02 12:33:20 +08:00
cmd
. command ( "file" )
. description ( "Print the active config file path" )
. action ( async ( ) = > {
await runConfigFile ( { } ) ;
} ) ;
2026-03-02 13:45:51 +08:00
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 ) } ) ;
} ) ;
2026-01-16 06:57:16 +00:00
}