fix: bind CLI config file values to cobra flags#1566
fix: bind CLI config file values to cobra flags#1566ExpertVagabond wants to merge 1 commit intokagent-dev:mainfrom
Conversation
Config file values (~/.kagent/config.yaml) were ignored because cobra flag defaults always took precedence. This wires viper-backed config into the flag system so the precedence is: CLI flag > env > config file > viper default. Adds BindFlags() to explicitly map dash-separated flag names to underscore-separated config keys, and Apply() to populate the shared Config struct from the merged viper state before any subcommand runs. Includes tests verifying config file values are used and that explicit CLI flags still override them.
There was a problem hiding this comment.
Pull request overview
Fixes CLI config precedence so values from ~/.kagent/config.yaml are not silently ignored when corresponding cobra flags exist, by explicitly binding cobra flag names to viper/config keys and applying merged viper state before command execution.
Changes:
- Add explicit cobra-flag-to-viper-key bindings (
kagent-url→kagent_url, etc.). - Introduce
config.Apply()and run it viarootCmd.PersistentPreRunEso subcommands see merged config. - Add tests validating config-file defaults are honored and CLI flags still override.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| go/core/cli/internal/config/config.go | Adds BindFlags() for explicit key mapping and Apply() to copy merged viper state into the shared Config. |
| go/core/cli/cmd/kagent/main.go | Calls BindFlags() and wires Apply() into PersistentPreRunE so config is populated after flag parsing. |
| go/core/cli/internal/config/config_test.go | New tests asserting config-file values apply when flags aren’t set and CLI flags override config. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| func BindFlags(flags *pflag.FlagSet) { | ||
| viper.BindPFlag("kagent_url", flags.Lookup("kagent-url")) //nolint:errcheck | ||
| viper.BindPFlag("namespace", flags.Lookup("namespace")) //nolint:errcheck | ||
| viper.BindPFlag("output_format", flags.Lookup("output-format")) //nolint:errcheck | ||
| viper.BindPFlag("verbose", flags.Lookup("verbose")) //nolint:errcheck | ||
| viper.BindPFlag("timeout", flags.Lookup("timeout")) //nolint:errcheck |
There was a problem hiding this comment.
BindFlags ignores the return value from viper.BindPFlag and also passes flags.Lookup(...) without checking for nil. If a flag is renamed/removed (or a different FlagSet is passed), the binding will silently fail and config precedence will regress. Please handle and surface these errors (e.g., return an error from BindFlags and check it in main), and explicitly error when Lookup returns nil for an expected flag.
| func BindFlags(flags *pflag.FlagSet) { | |
| viper.BindPFlag("kagent_url", flags.Lookup("kagent-url")) //nolint:errcheck | |
| viper.BindPFlag("namespace", flags.Lookup("namespace")) //nolint:errcheck | |
| viper.BindPFlag("output_format", flags.Lookup("output-format")) //nolint:errcheck | |
| viper.BindPFlag("verbose", flags.Lookup("verbose")) //nolint:errcheck | |
| viper.BindPFlag("timeout", flags.Lookup("timeout")) //nolint:errcheck | |
| func BindFlags(flags *pflag.FlagSet) error { | |
| if flags == nil { | |
| return fmt.Errorf("BindFlags: flags must not be nil") | |
| } | |
| bind := func(key, flagName string) error { | |
| f := flags.Lookup(flagName) | |
| if f == nil { | |
| return fmt.Errorf("BindFlags: expected flag %q not found in FlagSet", flagName) | |
| } | |
| if err := viper.BindPFlag(key, f); err != nil { | |
| return fmt.Errorf("BindFlags: failed to bind viper key %q to flag %q: %w", key, flagName, err) | |
| } | |
| return nil | |
| } | |
| if err := bind("kagent_url", "kagent-url"); err != nil { | |
| return err | |
| } | |
| if err := bind("namespace", "namespace"); err != nil { | |
| return err | |
| } | |
| if err := bind("output_format", "output-format"); err != nil { | |
| return err | |
| } | |
| if err := bind("verbose", "verbose"); err != nil { | |
| return err | |
| } | |
| if err := bind("timeout", "timeout"); err != nil { | |
| return err | |
| } | |
| return nil |
| // Apply populates the given Config from the merged viper state (config file + | ||
| // env + CLI flags). Call this after flag parsing to ensure cfg reflects all | ||
| // sources with the correct precedence: CLI flag > env > config file > default. | ||
| func Apply(cfg *Config) error { |
There was a problem hiding this comment.
The new Apply() docstring and precedence statement mention merging env vars, but this package currently only binds USER_ID and does not enable AutomaticEnv / BindEnv for the config keys (kagent_url, namespace, output_format, verbose, timeout). Either implement env var support for these keys (and document the expected env names), or update this comment so it matches actual behavior.
| // Populate cfg from the merged viper state (config file + env + flags) | ||
| // before any subcommand runs, ensuring all commands see config file values. | ||
| originalPreRunE := rootCmd.PersistentPreRunE | ||
| rootCmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error { | ||
| if err := config.Apply(cfg); err != nil { | ||
| return fmt.Errorf("error applying config: %w", err) | ||
| } | ||
| if originalPreRunE != nil { | ||
| return originalPreRunE(cmd, args) | ||
| } | ||
| return nil |
There was a problem hiding this comment.
Calling config.Apply(cfg) from rootCmd.PersistentPreRunE overwrites the already-parsed flag-backed cfg fields with viper state. This breaks command-local overrides of shared config fields that are not bound to viper. For example, deployCmd defines its own --namespace flag (separate from the persistent one), so kagent deploy --namespace X will be parsed into cfg.Namespace and then potentially clobbered by Apply() since viper is only bound to the persistent flag. Consider binding using the executing command's flag set (e.g., in this PreRunE: BindFlags(cmd.Flags()) before Apply), or remove the command-local --namespace and rely on the persistent flag so CLI overrides are preserved.
Summary
Fixes #973 — CLI config file values (
~/.kagent/config.yaml) were silently ignored because cobra flag defaults always took precedence over viper-backed config.Changes
config.BindFlags()— explicitly binds dash-separated cobra flag names (kagent-url,output-format) to underscore-separated viper/config keys (kagent_url,output_format)config.Apply()— populates the sharedConfigstruct from the merged viper state before any subcommand runsPersistentPreRunEinmain.go— callsApply()after flag parsing so all commands see config file valuesPrecedence (unchanged semantics, now actually works)
CLI flag > environment variable > config file > viper default
Testing