feat: wire config system into main — TOML config now active

config.Load() called at startup. Layered: defaults → global
(~/.config/gnoma/config.toml) → project (.gnoma/config.toml)
→ env vars. CLI flags override config values.

Config drives:
- provider.default + provider.model as defaults
- provider.api_keys for key resolution
- provider.endpoints for custom base URLs
- permission.mode + permission.rules loaded into checker
- tools.bash_timeout passed to bash tool

Example .gnoma/config.toml:
  [provider]
  default = "ollama"
  model = "qwen3:14b"
  [permission]
  mode = "bypass"
  [[permission.rules]]
  tool = "bash"
  pattern = "rm -rf"
  action = "deny"
This commit is contained in:
2026-04-03 17:38:58 +02:00
parent 17bd84d56b
commit cfa87f1c1b
3 changed files with 83 additions and 9 deletions

View File

@@ -12,6 +12,7 @@ import (
"somegit.dev/Owlibou/gnoma/internal/engine"
"encoding/json"
gnomacfg "somegit.dev/Owlibou/gnoma/internal/config"
"somegit.dev/Owlibou/gnoma/internal/permission"
"somegit.dev/Owlibou/gnoma/internal/provider"
"somegit.dev/Owlibou/gnoma/internal/router"
@@ -58,20 +59,54 @@ func main() {
}
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: logLevel}))
// Resolve API key (local providers don't need one)
// Load config (defaults → global project → env vars)
cfg, err := gnomacfg.Load()
if err != nil {
fmt.Fprintf(os.Stderr, "warning: config load: %v\n", err)
defaults := gnomacfg.Defaults()
cfg = &defaults
}
logger.Debug("config loaded",
"provider", cfg.Provider.Default,
"model", cfg.Provider.Model,
"keys", len(cfg.Provider.APIKeys),
"perm_mode", cfg.Permission.Mode,
"perm_rules", len(cfg.Permission.Rules),
)
// CLI flags override config
if !isFlagSet("provider") {
*providerName = cfg.Provider.Default
}
if !isFlagSet("model") && cfg.Provider.Model != "" {
*model = cfg.Provider.Model
}
if !isFlagSet("permission") && cfg.Permission.Mode != "" {
*permMode = cfg.Permission.Mode
}
// Resolve API key: CLI flag → config → env vars
localProviders := map[string]bool{"ollama": true, "llamacpp": true}
key := *apiKey
if key == "" {
if cfgKey, ok := cfg.Provider.APIKeys[*providerName]; ok && cfgKey != "" {
key = cfgKey
}
}
if key == "" {
key = resolveAPIKey(*providerName)
}
localProviders := map[string]bool{"ollama": true, "llamacpp": true}
if key == "" && !localProviders[*providerName] {
fmt.Fprintf(os.Stderr, "error: no API key for provider %q\nSet %s environment variable or use --api-key\n",
*providerName, envKeyFor(*providerName))
os.Exit(1)
}
// Resolve base URL from config endpoints
baseURL := cfg.Provider.Endpoints[*providerName]
// Create provider
prov, err := createProvider(*providerName, key, *model)
prov, err := createProvider(*providerName, key, *model, baseURL)
if err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
@@ -95,8 +130,12 @@ func main() {
"runtimes", len(inventory.Runtimes),
)
// Re-register bash tool with aliases
reg.Register(bash.New(bash.WithAliases(aliases)))
// Re-register bash tool with aliases and config timeout
bashOpts := []bash.Option{bash.WithAliases(aliases)}
if cfg.Tools.BashTimeout.Duration() > 0 {
bashOpts = append(bashOpts, bash.WithTimeout(cfg.Tools.BashTimeout.Duration()))
}
reg.Register(bash.New(bashOpts...))
// Register system_info tool backed by the inventory
reg.Register(sysinfo.New(inventory))
@@ -139,7 +178,16 @@ func main() {
fmt.Scanln(&response)
return strings.ToLower(response) == "y" || strings.ToLower(response) == "yes", nil
}
permChecker := permission.NewChecker(permission.Mode(*permMode), nil, pipePromptFn)
// Convert config rules to permission rules
var permRules []permission.Rule
for _, r := range cfg.Permission.Rules {
permRules = append(permRules, permission.Rule{
Tool: r.Tool,
Pattern: r.Pattern,
Action: permission.Action(r.Action),
})
}
permChecker := permission.NewChecker(permission.Mode(*permMode), permRules, pipePromptFn)
// Build system prompt with compact inventory summary
systemPrompt := *system
@@ -292,10 +340,21 @@ func resolveAPIKey(providerName string) string {
return ""
}
func createProvider(name, apiKey, model string) (provider.Provider, error) {
func isFlagSet(name string) bool {
set := false
flag.Visit(func(f *flag.Flag) {
if f.Name == name {
set = true
}
})
return set
}
func createProvider(name, apiKey, model, baseURL string) (provider.Provider, error) {
cfg := provider.ProviderConfig{
APIKey: apiKey,
Model: model,
APIKey: apiKey,
Model: model,
BaseURL: baseURL,
}
switch name {

View File

@@ -5,9 +5,21 @@ import "time"
// Config is the top-level configuration.
type Config struct {
Provider ProviderSection `toml:"provider"`
Permission PermissionSection `toml:"permission"`
Tools ToolsSection `toml:"tools"`
}
type PermissionSection struct {
Mode string `toml:"mode"`
Rules []PermissionRule `toml:"rules"`
}
type PermissionRule struct {
Tool string `toml:"tool"`
Pattern string `toml:"pattern"`
Action string `toml:"action"`
}
type ProviderSection struct {
Default string `toml:"default"`
Model string `toml:"model"`

View File

@@ -11,6 +11,9 @@ func Defaults() Config {
APIKeys: make(map[string]string),
Endpoints: make(map[string]string),
},
Permission: PermissionSection{
Mode: "default",
},
Tools: ToolsSection{
BashTimeout: Duration(30 * time.Second),
MaxFileSize: 1 << 20, // 1MB