From cfa87f1c1b1c1f27928b38365cea9d145af36447 Mon Sep 17 00:00:00 2001 From: vikingowl Date: Fri, 3 Apr 2026 17:38:58 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20wire=20config=20system=20into=20main=20?= =?UTF-8?q?=E2=80=94=20TOML=20config=20now=20active?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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" --- cmd/gnoma/main.go | 77 ++++++++++++++++++++++++++++++++----- internal/config/config.go | 12 ++++++ internal/config/defaults.go | 3 ++ 3 files changed, 83 insertions(+), 9 deletions(-) diff --git a/cmd/gnoma/main.go b/cmd/gnoma/main.go index 27e422d..816e1d7 100644 --- a/cmd/gnoma/main.go +++ b/cmd/gnoma/main.go @@ -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 { diff --git a/internal/config/config.go b/internal/config/config.go index 426d453..7f72578 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -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"` diff --git a/internal/config/defaults.go b/internal/config/defaults.go index 88db89c..2ad355a 100644 --- a/internal/config/defaults.go +++ b/internal/config/defaults.go @@ -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