package config import "time" // Config is the top-level configuration. type Config struct { // DefaultProfile names the profile loaded when no --profile flag is // passed. Only meaningful when ~/.config/gnoma/profiles/ exists; see // LoadWithProfile. DefaultProfile string `toml:"default_profile"` Provider ProviderSection `toml:"provider"` Permission PermissionSection `toml:"permission"` Tools ToolsSection `toml:"tools"` RateLimits RateLimitSection `toml:"rate_limits"` Security SecuritySection `toml:"security"` Session SessionSection `toml:"session"` SLM SLMSection `toml:"slm"` Router RouterSection `toml:"router"` Safety SafetySection `toml:"safety"` CLIAgents CLIAgentsSection `toml:"cli_agents"` Arms []ArmConfig `toml:"arms"` Hooks []HookConfig `toml:"hooks"` MCPServers []MCPServerConfig `toml:"mcp_servers"` Plugins PluginsSection `toml:"plugins"` TUI TUISection `toml:"tui"` } // SLMSection configures the optional small language model used for task // classification and low-complexity task execution. // // Backend selects how the SLM is reached: // - "auto" / "" — pick the best available local backend at startup // (Ollama → llama.cpp → llamafile) // - "ollama" — talk to a local Ollama daemon // - "llamacpp" — talk to a local llama.cpp server // - "llamafile" — gnoma manages the llamafile process itself // - "openaicompat" — any OpenAI-compatible URL (LM Studio, vLLM, etc.) // - "disabled" — skip the SLM entirely; classifier stays heuristic // // See docs/slm-backends.md for copy-paste presets. type SLMSection struct { Enabled bool `toml:"enabled"` Backend string `toml:"backend"` // auto | ollama | llamacpp | llamafile | openaicompat | disabled (empty = auto) Model string `toml:"model"` // model name (ollama/llamacpp/openaicompat); ignored for llamafile BaseURL string `toml:"base_url"` // server URL; defaults per-backend ModelURL string `toml:"model_url"` // llamafile-only: where to download the binary from DataDir string `toml:"data_dir"` // llamafile-only: where to put it (empty = XDG default) ExpectedSHA256 string `toml:"expected_sha256"` // llamafile-only: verify hash if non-empty StartupTimeout Duration `toml:"startup_timeout"` // llamafile-only: first-launch wait budget; 0 = default 5s // ClassifyTimeout caps each task-classification call to the SLM. // 0 here means "use the built-in default" (15s). Cold-start model // loads + thinking-mode first-token latency can easily exceed 5s // on smaller hardware, so the default is generous. Tune down to // 2-3s on fast setups, or up to 30s for very slow ones. ClassifyTimeout Duration `toml:"classify_timeout"` // RegisterAsArm controls whether the SLM model is registered as // a tier-0 execution arm in addition to its classifier role. // nil (absent) → true (preserve historical behaviour: SLM is // both classifier and an execution arm for trivial-complexity // prompts). Explicitly false → SLM is classifier-only; trivial // prompts route to other local arms instead. // // Set this to false when the SLM model is task-specialised // (FunctionGemma, embedding-only models, code-completion-tuned // models) and would produce wrong-shape output if asked to // answer a general prompt. Pointer type so the absent-value // case can be distinguished from explicit false. RegisterAsArm *bool `toml:"register_as_arm"` } // ArmConfig tunes routing for a single registered arm. Multiple [[arms]] // blocks may appear; each is matched by ID against the runtime arm // registry. An ID that doesn't match any registered arm logs a warning at // startup — typos here are otherwise silent. // // Example: // // [[arms]] // id = "anthropic/claude-opus-4-7" // strengths = ["security_review", "planning"] # task types this arm is preferred for // cost_weight = 0.3 # 1.0 = full cost penalty, 0 = ignore cost // // [[arms]] // id = "subprocess/claude" // strengths = ["orchestration"] // // Strength names map to router.TaskType via router.ParseTaskType — same // names the SLM classifier emits (snake_case or no separator both work). type ArmConfig struct { ID string `toml:"id"` Strengths []string `toml:"strengths"` CostWeight float64 `toml:"cost_weight"` } // CLIAgentsSection maps canonical CLI agent names to override binary names. // // Useful when a user has aliased the canonical binary — e.g. `claude-priv` // instead of `claude`, or `gemini-work` instead of `gemini` — and wants // gnoma's auto-discovery to find it. // // Example: // // [cli_agents] // claude = "claude-priv" # use claude-priv as the Claude Code binary // gemini = "gemini-work" // # vibe is unset → falls back to the canonical "vibe" name // // An empty value (e.g. `claude = ""`) is treated as "no override" — the // canonical name is used. type CLIAgentsSection map[string]string // RouterSection holds router-level overrides. Most routing decisions are // driven automatically by arm capabilities and the bandit; this section // exists for the rare overrides that don't fit elsewhere. // SafetySection controls the pre-launch dir-safety classifier — refuse // in system roots, warn+keypress in $HOME and other dumping grounds, // OK inside any git repo or project marker. Always shows a context // banner regardless of tier. See // docs/superpowers/plans/2026-05-23-startup-safety-banner.md. type SafetySection struct { // RefuseInSystemDirs gates the refuse path. When false, system // roots like / and /etc are treated as warn-tier instead of refuse. // Default: true. RefuseInSystemDirs *bool `toml:"refuse_in_system_dirs"` // WarnInHome gates the warn-tier check for $HOME and common // dumping grounds (~/Desktop, ~/Downloads, /tmp). When false, // these all become OK-tier (banner still shown). Default: true. WarnInHome *bool `toml:"warn_in_home"` // RequireProjectMarker, when true, treats any directory without // a recognized project marker as warn-tier (even inside a git // repo). Default: false — git repo is enough by default. RequireProjectMarker bool `toml:"require_project_marker"` } // ResolvedSafety returns the effective Safety settings with defaults // applied for any unset pointer fields. Pointer fields are used in the // struct so we can distinguish "user omitted the key" from "user set // it to false." func (s SafetySection) ResolvedSafety() ResolvedSafetySection { refuse := true if s.RefuseInSystemDirs != nil { refuse = *s.RefuseInSystemDirs } warn := true if s.WarnInHome != nil { warn = *s.WarnInHome } return ResolvedSafetySection{ RefuseInSystemDirs: refuse, WarnInHome: warn, RequireProjectMarker: s.RequireProjectMarker, } } // ResolvedSafetySection is the SafetySection with defaults applied. // Consumers (cmd/gnoma/main.go, internal/safety) read this rather than // the raw config to avoid re-deriving defaults at each call site. type ResolvedSafetySection struct { RefuseInSystemDirs bool WarnInHome bool RequireProjectMarker bool } type RouterSection struct { // ForceTwoStage forces the two-stage tool-routing path regardless of // arm context window. Useful for debugging or for forcing the behavior // on a large local model. Defaults to false: two-stage activates // automatically on local arms with context window <= 16k. ForceTwoStage bool `toml:"force_two_stage"` // Prefer biases routing toward local arms ("local"), cloud arms // ("cloud"), or leaves the tier-based selection unchanged ("auto"). // Default: "auto". Implemented as a soft score multiplier — does // not hard-filter the dispreferred set. Forced arms (--provider X) // and incognito take priority over this knob. See // docs/superpowers/plans/2026-05-23-prefer-routing-policy.md. Prefer string `toml:"prefer"` // Bandit exposes the selector's tuning knobs. Defaults preserve // previous hard-coded behaviour exactly; only set these when you // need to tune the EMA quality tracker for an unusual workload. Bandit BanditSection `toml:"bandit"` } // BanditSection holds the scoring knobs for the EMA quality tracker // and the score blend used by the selector. Each field has a sentinel // zero value that means "use the built-in default" so an empty TOML // block is byte-identical to pre-config behaviour. See // internal/router/feedback.go and internal/router/selector.go for the // formulas these knobs feed into. type BanditSection struct { // QualityAlpha is the EMA smoothing factor for arm-quality // observations. Larger values weight recent observations more. // Default: 0.3 (~3-sample memory). 0.0 here means "use default". QualityAlpha float64 `toml:"quality_alpha"` // MinObservations is the minimum number of samples required // before observed EMA overrides the heuristic fallback. Default: // 3. 0 here means "use default". MinObservations int `toml:"min_observations"` // ObservedWeight is the weight of the observed EMA in the // observed/heuristic blend inside scoreArm: the final quality is // `observed*W + heuristic*(1-W)`. Default: 0.7. 0.0 here means // "use default". ObservedWeight float64 `toml:"observed_weight"` // StrengthBonus is the quality bonus added when an arm declares // the current task type in its Strengths list. Default: 0.15. // 0.0 here means "use default". StrengthBonus float64 `toml:"strength_bonus"` } // MCPServerConfig defines an MCP server to start and connect to. // // Example: // // [[mcp_servers]] // name = "git" // command = "mcp-server-git" // args = ["--repo", "."] // env = { GIT_DIR = ".git" } // timeout = "30s" // replace_default = { exec = "bash" } # MCP tool "exec" replaces built-in "bash" type MCPServerConfig struct { Name string `toml:"name"` Command string `toml:"command"` Args []string `toml:"args"` Env map[string]string `toml:"env"` Timeout string `toml:"timeout"` ReplaceDefault map[string]string `toml:"replace_default"` // MCP tool name → built-in name ToolPolicy map[string]MCPToolPolicy `toml:"tool_policy"` // MCP tool name → policy } type MCPToolPolicy struct { PathArgs []string `toml:"path_args"` } // PluginsSection controls plugin loading. // // Example: // // [plugins] // enabled = ["git-tools", "docker-tools"] // disabled = ["experimental-plugin"] type PluginsSection struct { Enabled []string `toml:"enabled"` Disabled []string `toml:"disabled"` } // HookConfig is a single hook entry from TOML config. // // Example: // // [[hooks]] // name = "block-dangerous-bash" // event = "pre_tool_use" // type = "command" // exec = "bash-safety-check.sh" // tool_pattern = "bash*" // timeout = "10s" // fail_open = false type HookConfig struct { Name string `toml:"name"` Event string `toml:"event"` Type string `toml:"type"` Exec string `toml:"exec"` Timeout string `toml:"timeout"` FailOpen bool `toml:"fail_open"` ToolPattern string `toml:"tool_pattern"` } type SessionSection struct { MaxKeep int `toml:"max_keep"` } // SecuritySection configures the secret scanner and firewall. // // Example config: // // [security] // entropy_threshold = 4.5 // entropy_safelist = ["uuid", "sha_hex", "iso8601", "url"] // // [[security.patterns]] // name = "internal_token" // regex = "mycompany_[a-zA-Z0-9]{32}" // action = "redact" // // entropy_safelist names known-safe shapes that bypass the entropy scorer // (Phase F-1 FP reduction). Empty / unset preserves pre-F-1 behavior. type SecuritySection struct { EntropyThreshold float64 `toml:"entropy_threshold"` RedactHighEntropy bool `toml:"redact_high_entropy"` EntropySafelist []string `toml:"entropy_safelist"` Patterns []PatternConfig `toml:"patterns"` } type PatternConfig struct { Name string `toml:"name"` Regex string `toml:"regex"` Action string `toml:"action"` // "redact" (default), "block", "warn" } 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"` MaxTokens int64 `toml:"max_tokens"` Temperature *float64 `toml:"temperature"` APIKeys map[string]string `toml:"api_keys"` Endpoints map[string]string `toml:"endpoints"` } type ToolsSection struct { BashTimeout Duration `toml:"bash_timeout"` MaxFileSize int64 `toml:"max_file_size"` } // RateLimitSection allows overriding default rate limits per provider. // // Example config: // // [rate_limits.mistral] // tier = "starter" // rps = 1 // spend_cap = 20.0 // // [rate_limits.anthropic] // tier = "tier2" // rpm = 1000 // itpm = 450000 // otpm = 90000 type RateLimitSection map[string]RateLimitOverride type RateLimitOverride struct { Tier string `toml:"tier"` RPS float64 `toml:"rps"` RPM int `toml:"rpm"` RPD int `toml:"rpd"` TPM int `toml:"tpm"` ITPM int `toml:"itpm"` OTPM int `toml:"otpm"` TokensMonth int64 `toml:"tokens_month"` SpendCap float64 `toml:"spend_cap"` } // Duration wraps time.Duration for TOML string parsing (e.g. "30s", "5m"). type Duration time.Duration func (d *Duration) UnmarshalText(text []byte) error { parsed, err := time.ParseDuration(string(text)) if err != nil { return err } *d = Duration(parsed) return nil } func (d Duration) Duration() time.Duration { return time.Duration(d) } type TUISection struct { Theme string `toml:"theme"` Vim bool `toml:"vim"` }