ScuttleBot

scuttlebot / internal / config / config.go
Source Blame History 519 lines
c369cd5… lmata 1 // Package config defines scuttlebot's configuration schema.
cadb504… lmata 2 package config
c369cd5… lmata 3
61c045e… lmata 4 import (
61c045e… lmata 5 "fmt"
61c045e… lmata 6 "os"
5705614… lmata 7 "time"
61c045e… lmata 8
61c045e… lmata 9 "gopkg.in/yaml.v3"
61c045e… lmata 10 )
2d8a379… lmata 11
c369cd5… lmata 12 // Config is the top-level scuttlebot configuration.
c369cd5… lmata 13 type Config struct {
763c873… lmata 14 Ergo ErgoConfig `yaml:"ergo"`
763c873… lmata 15 Datastore DatastoreConfig `yaml:"datastore"`
763c873… lmata 16 Bridge BridgeConfig `yaml:"bridge"`
763c873… lmata 17 TLS TLSConfig `yaml:"tls"`
763c873… lmata 18 LLM LLMConfig `yaml:"llm"`
763c873… lmata 19 Topology TopologyConfig `yaml:"topology"`
763c873… lmata 20 History ConfigHistoryConfig `yaml:"config_history"`
763c873… lmata 21 AgentPolicy AgentPolicyConfig `yaml:"agent_policy" json:"agent_policy"`
763c873… lmata 22 Logging LoggingConfig `yaml:"logging" json:"logging"`
2d8a379… lmata 23
2d8a379… lmata 24 // APIAddr is the address for scuttlebot's own HTTP management API.
5ac549c… lmata 25 // Ignored when TLS.Domain is set (HTTPS runs on :443, HTTP on :80).
2d8a379… lmata 26 // Default: ":8080"
2d8a379… lmata 27 APIAddr string `yaml:"api_addr"`
550b35e… lmata 28
550b35e… lmata 29 // MCPAddr is the address for the MCP server.
550b35e… lmata 30 // Default: ":8081"
550b35e… lmata 31 MCPAddr string `yaml:"mcp_addr"`
763c873… lmata 32 }
763c873… lmata 33
763c873… lmata 34 // AgentPolicyConfig defines requirements applied to all registering agents.
763c873… lmata 35 type AgentPolicyConfig struct {
763c873… lmata 36 RequireCheckin bool `yaml:"require_checkin" json:"require_checkin"`
763c873… lmata 37 CheckinChannel string `yaml:"checkin_channel" json:"checkin_channel"`
763c873… lmata 38 RequiredChannels []string `yaml:"required_channels" json:"required_channels"`
763c873… lmata 39 }
763c873… lmata 40
763c873… lmata 41 // LoggingConfig configures message logging.
763c873… lmata 42 type LoggingConfig struct {
763c873… lmata 43 Enabled bool `yaml:"enabled" json:"enabled"`
763c873… lmata 44 Dir string `yaml:"dir" json:"dir"`
8332dd8… lmata 45 Format string `yaml:"format" json:"format"` // "jsonl" | "csv" | "text"
8332dd8… lmata 46 Rotation string `yaml:"rotation" json:"rotation"` // "none" | "daily" | "weekly" | "size"
763c873… lmata 47 MaxSizeMB int `yaml:"max_size_mb" json:"max_size_mb"`
763c873… lmata 48 PerChannel bool `yaml:"per_channel" json:"per_channel"`
763c873… lmata 49 MaxAgeDays int `yaml:"max_age_days" json:"max_age_days"`
0619d47… lmata 50 }
0619d47… lmata 51
17e2c1d… lmata 52 // ConfigHistoryConfig controls config write-back history retention.
17e2c1d… lmata 53 type ConfigHistoryConfig struct {
17e2c1d… lmata 54 // Keep is the number of config snapshots to retain in Dir.
17e2c1d… lmata 55 // 0 disables history. Default: 20.
0619d47… lmata 56 Keep int `yaml:"keep" json:"keep"`
17e2c1d… lmata 57
17e2c1d… lmata 58 // Dir is the directory for config snapshots.
17e2c1d… lmata 59 // Default: {ergo.data_dir}/config-history
0619d47… lmata 60 Dir string `yaml:"dir" json:"dir,omitempty"`
5ac549c… lmata 61 }
5ac549c… lmata 62
5ac549c… lmata 63 // LLMConfig configures the omnibus LLM gateway used by oracle and any other
5ac549c… lmata 64 // bot or service that needs language model access.
5ac549c… lmata 65 type LLMConfig struct {
5ac549c… lmata 66 // Backends is the list of configured LLM backends.
5ac549c… lmata 67 // Each backend has a unique Name used to reference it from bot configs.
5ac549c… lmata 68 Backends []LLMBackendConfig `yaml:"backends"`
5ac549c… lmata 69 }
5ac549c… lmata 70
5ac549c… lmata 71 // LLMBackendConfig configures a single LLM backend instance.
5ac549c… lmata 72 type LLMBackendConfig struct {
5ac549c… lmata 73 // Name is a unique identifier for this backend (e.g. "openai-main", "local-ollama").
5ac549c… lmata 74 // Used when referencing the backend from bot configs.
5ac549c… lmata 75 Name string `yaml:"name"`
5ac549c… lmata 76
5ac549c… lmata 77 // Backend is the provider type. Supported values:
5ac549c… lmata 78 // Native: anthropic, gemini, bedrock, ollama
5ac549c… lmata 79 // OpenAI-compatible: openai, openrouter, together, groq, fireworks, mistral,
5ac549c… lmata 80 // ai21, huggingface, deepseek, cerebras, xai,
5ac549c… lmata 81 // litellm, lmstudio, jan, localai, vllm, anythingllm
5ac549c… lmata 82 Backend string `yaml:"backend"`
5ac549c… lmata 83
5ac549c… lmata 84 // APIKey is the authentication key for cloud backends.
5ac549c… lmata 85 APIKey string `yaml:"api_key"`
5ac549c… lmata 86
5ac549c… lmata 87 // BaseURL overrides the default base URL for OpenAI-compatible backends.
5ac549c… lmata 88 // Required for custom self-hosted endpoints without a known default.
5ac549c… lmata 89 BaseURL string `yaml:"base_url"`
5ac549c… lmata 90
5ac549c… lmata 91 // Model is the default model ID. If empty, the first discovered model
5ac549c… lmata 92 // that passes the allow/block filter is used.
5ac549c… lmata 93 Model string `yaml:"model"`
5ac549c… lmata 94
5ac549c… lmata 95 // Region is the AWS region (e.g. "us-east-1"). Bedrock only.
5ac549c… lmata 96 Region string `yaml:"region"`
5ac549c… lmata 97
5ac549c… lmata 98 // AWSKeyID is the AWS access key ID. Bedrock only.
5ac549c… lmata 99 AWSKeyID string `yaml:"aws_key_id"`
5ac549c… lmata 100
5ac549c… lmata 101 // AWSSecretKey is the AWS secret access key. Bedrock only.
5ac549c… lmata 102 AWSSecretKey string `yaml:"aws_secret_key"`
5ac549c… lmata 103
5ac549c… lmata 104 // Allow is a list of regex patterns. If non-empty, only model IDs matching
5ac549c… lmata 105 // at least one pattern are returned by model discovery.
5ac549c… lmata 106 Allow []string `yaml:"allow"`
5ac549c… lmata 107
5ac549c… lmata 108 // Block is a list of regex patterns. Matching model IDs are excluded
5ac549c… lmata 109 // from model discovery results.
5ac549c… lmata 110 Block []string `yaml:"block"`
5ac549c… lmata 111
5ac549c… lmata 112 // Default marks this backend as the one used when no backend is specified
5ac549c… lmata 113 // in a bot's config. Only one backend should have Default: true.
5ac549c… lmata 114 Default bool `yaml:"default"`
5ac549c… lmata 115 }
5ac549c… lmata 116
5ac549c… lmata 117 // TLSConfig configures automatic HTTPS via Let's Encrypt.
5ac549c… lmata 118 type TLSConfig struct {
5ac549c… lmata 119 // Domain enables TLS. When set, scuttlebot obtains a certificate from
5ac549c… lmata 120 // Let's Encrypt for this domain and serves HTTPS on :443.
5ac549c… lmata 121 Domain string `yaml:"domain"`
5ac549c… lmata 122
5ac549c… lmata 123 // Email is sent to Let's Encrypt for certificate expiry notifications.
5ac549c… lmata 124 Email string `yaml:"email"`
5ac549c… lmata 125
5ac549c… lmata 126 // CertDir is the directory for the certificate cache.
5ac549c… lmata 127 // Default: {Ergo.DataDir}/certs
5ac549c… lmata 128 CertDir string `yaml:"cert_dir"`
5ac549c… lmata 129
5ac549c… lmata 130 // AllowInsecure keeps plain HTTP running on :80 alongside HTTPS.
5ac549c… lmata 131 // The ACME HTTP-01 challenge always runs on :80 regardless.
5ac549c… lmata 132 // Default: true
5ac549c… lmata 133 AllowInsecure bool `yaml:"allow_insecure"`
c369cd5… lmata 134 }
c369cd5… lmata 135
c369cd5… lmata 136 // ErgoConfig holds settings for the managed Ergo IRC server.
c369cd5… lmata 137 type ErgoConfig struct {
2d8a379… lmata 138 // External disables subprocess management. When true, scuttlebot expects
2d8a379… lmata 139 // ergo to already be running and reachable at APIAddr and IRCAddr.
2d8a379… lmata 140 // Use this in Docker/K8s deployments where ergo runs as a separate container.
2d8a379… lmata 141 External bool `yaml:"external"`
2d8a379… lmata 142
c369cd5… lmata 143 // BinaryPath is the path to the ergo binary. Defaults to "ergo" (looks in PATH).
2d8a379… lmata 144 // Unused when External is true.
c369cd5… lmata 145 BinaryPath string `yaml:"binary_path"`
c369cd5… lmata 146
c369cd5… lmata 147 // DataDir is the directory where Ergo stores ircd.db and generated config.
2d8a379… lmata 148 // Unused when External is true.
c369cd5… lmata 149 DataDir string `yaml:"data_dir"`
c369cd5… lmata 150
c369cd5… lmata 151 // NetworkName is the human-readable IRC network name.
c369cd5… lmata 152 NetworkName string `yaml:"network_name"`
c369cd5… lmata 153
c369cd5… lmata 154 // ServerName is the IRC server hostname (e.g. "irc.example.com").
c369cd5… lmata 155 ServerName string `yaml:"server_name"`
c369cd5… lmata 156
c369cd5… lmata 157 // IRCAddr is the address Ergo listens for IRC connections on.
c369cd5… lmata 158 // Default: "127.0.0.1:6667" (loopback plaintext for private networks).
0e78954… lmata 159 // Set to ":6667" or ":6697" to accept connections from outside the host.
c369cd5… lmata 160 IRCAddr string `yaml:"irc_addr"`
c369cd5… lmata 161
c369cd5… lmata 162 // APIAddr is the address of Ergo's HTTP management API.
c369cd5… lmata 163 // Default: "127.0.0.1:8089" (loopback only).
c369cd5… lmata 164 APIAddr string `yaml:"api_addr"`
c369cd5… lmata 165
c369cd5… lmata 166 // APIToken is the bearer token for Ergo's HTTP API.
c369cd5… lmata 167 // scuttlebot generates this on first start and stores it.
c369cd5… lmata 168 APIToken string `yaml:"api_token"`
0e78954… lmata 169
0e78954… lmata 170 // RequireSASL enforces SASL authentication for all IRC connections.
0e78954… lmata 171 // When true, only agents and users with registered NickServ accounts
0e78954… lmata 172 // can connect. Recommended for public deployments.
0e78954… lmata 173 // Default: false
0e78954… lmata 174 RequireSASL bool `yaml:"require_sasl"`
0e78954… lmata 175
0e78954… lmata 176 // DefaultChannelModes sets channel modes applied when a new channel is
0e78954… lmata 177 // created. Common values: "+n" (no external messages), "+Rn" (registered
0e78954… lmata 178 // users only). Default: "+n"
0e78954… lmata 179 DefaultChannelModes string `yaml:"default_channel_modes"`
0e78954… lmata 180
18e8fef… lmata 181 // TLSDomain enables a public TLS listener on TLSAddr with Let's Encrypt
18e8fef… lmata 182 // (ACME TLS-ALPN-01). When set, IRCAddr is kept as an internal plaintext
18e8fef… lmata 183 // listener for system bots, and a second TLS listener is added for
18e8fef… lmata 184 // external agents. Leave empty to use IRCAddr as the only listener.
18e8fef… lmata 185 TLSDomain string `yaml:"tls_domain"`
18e8fef… lmata 186
18e8fef… lmata 187 // TLSAddr is the address for the public TLS IRC listener.
18e8fef… lmata 188 // Only used when TLSDomain is set. Default: "0.0.0.0:6697"
18e8fef… lmata 189 TLSAddr string `yaml:"tls_addr"`
18e8fef… lmata 190
c369cd5… lmata 191 // History configures persistent message history storage.
c369cd5… lmata 192 History HistoryConfig `yaml:"history"`
c369cd5… lmata 193 }
c369cd5… lmata 194
c369cd5… lmata 195 // HistoryConfig configures Ergo's persistent message history.
c369cd5… lmata 196 type HistoryConfig struct {
c369cd5… lmata 197 // Enabled enables persistent history storage.
c369cd5… lmata 198 Enabled bool `yaml:"enabled"`
c369cd5… lmata 199
c369cd5… lmata 200 // PostgresDSN is the Postgres connection string for persistent history.
c369cd5… lmata 201 // Recommended. If empty and Enabled is true, MySQL config is used instead.
c369cd5… lmata 202 PostgresDSN string `yaml:"postgres_dsn"`
c369cd5… lmata 203
c369cd5… lmata 204 // MySQL is the MySQL connection config for persistent history.
c369cd5… lmata 205 MySQL MySQLConfig `yaml:"mysql"`
c369cd5… lmata 206 }
c369cd5… lmata 207
c369cd5… lmata 208 // MySQLConfig holds MySQL connection settings for Ergo history.
c369cd5… lmata 209 type MySQLConfig struct {
c369cd5… lmata 210 Host string `yaml:"host"`
c369cd5… lmata 211 Port int `yaml:"port"`
c369cd5… lmata 212 User string `yaml:"user"`
c369cd5… lmata 213 Password string `yaml:"password"`
c369cd5… lmata 214 Database string `yaml:"database"`
c369cd5… lmata 215 }
c369cd5… lmata 216
d74d207… lmata 217 // BridgeConfig configures the IRC bridge bot that powers the web chat UI.
d74d207… lmata 218 type BridgeConfig struct {
d74d207… lmata 219 // Enabled controls whether the bridge bot starts. Default: true.
d74d207… lmata 220 Enabled bool `yaml:"enabled"`
d74d207… lmata 221
d74d207… lmata 222 // Nick is the IRC nick for the bridge bot. Default: "bridge".
d74d207… lmata 223 Nick string `yaml:"nick"`
d74d207… lmata 224
d74d207… lmata 225 // Password is the SASL PLAIN passphrase for the bridge's NickServ account.
d74d207… lmata 226 // Auto-generated on first start if empty.
d74d207… lmata 227 Password string `yaml:"password"`
d74d207… lmata 228
d74d207… lmata 229 // Channels is the list of IRC channels the bridge joins on startup.
d74d207… lmata 230 Channels []string `yaml:"channels"`
d74d207… lmata 231
d74d207… lmata 232 // BufferSize is the number of messages to keep per channel. Default: 200.
d74d207… lmata 233 BufferSize int `yaml:"buffer_size"`
5ac549c… lmata 234
5ac549c… lmata 235 // WebUserTTLMinutes controls how long HTTP bridge sender nicks remain visible
5ac549c… lmata 236 // in the channel user list after their last post. Default: 5.
5ac549c… lmata 237 WebUserTTLMinutes int `yaml:"web_user_ttl_minutes"`
d74d207… lmata 238 }
d74d207… lmata 239
c369cd5… lmata 240 // DatastoreConfig configures scuttlebot's own state store (separate from Ergo).
c369cd5… lmata 241 type DatastoreConfig struct {
c369cd5… lmata 242 // Driver is "sqlite" or "postgres". Default: "sqlite".
c369cd5… lmata 243 Driver string `yaml:"driver"`
c369cd5… lmata 244
c369cd5… lmata 245 // DSN is the data source name.
c369cd5… lmata 246 // For sqlite: path to the .db file.
c369cd5… lmata 247 // For postgres: connection string.
c369cd5… lmata 248 DSN string `yaml:"dsn"`
5ac549c… lmata 249 }
5ac549c… lmata 250
5705614… lmata 251 // TopologyConfig is the top-level channel topology declaration.
5705614… lmata 252 // It defines static channels provisioned at startup and dynamic channel type
5705614… lmata 253 // rules applied when agents create channels at runtime.
5705614… lmata 254 type TopologyConfig struct {
d6520d1… lmata 255 // Nick is the IRC nick used by the topology manager to provision channels
d6520d1… lmata 256 // via ChanServ. Defaults to "topology".
0619d47… lmata 257 Nick string `yaml:"nick" json:"nick"`
d6520d1… lmata 258
5705614… lmata 259 // Channels are static channels provisioned at daemon startup.
0619d47… lmata 260 Channels []StaticChannelConfig `yaml:"channels" json:"channels"`
5705614… lmata 261
5705614… lmata 262 // Types are prefix-based rules applied to dynamically created channels.
5705614… lmata 263 // The first matching prefix wins.
0619d47… lmata 264 Types []ChannelTypeConfig `yaml:"types" json:"types"`
5705614… lmata 265 }
5705614… lmata 266
5705614… lmata 267 // StaticChannelConfig describes a channel that is provisioned at startup.
5705614… lmata 268 type StaticChannelConfig struct {
5705614… lmata 269 // Name is the full channel name including the # prefix (e.g. "#general").
0619d47… lmata 270 Name string `yaml:"name" json:"name"`
5705614… lmata 271
5705614… lmata 272 // Topic is the initial channel topic.
0619d47… lmata 273 Topic string `yaml:"topic" json:"topic,omitempty"`
5705614… lmata 274
5705614… lmata 275 // Ops is a list of nicks to grant channel operator (+o) access.
0619d47… lmata 276 Ops []string `yaml:"ops" json:"ops,omitempty"`
5705614… lmata 277
5705614… lmata 278 // Voice is a list of nicks to grant voice (+v) access.
0619d47… lmata 279 Voice []string `yaml:"voice" json:"voice,omitempty"`
5705614… lmata 280
5705614… lmata 281 // Autojoin is a list of bot nicks to invite when the channel is provisioned.
0619d47… lmata 282 Autojoin []string `yaml:"autojoin" json:"autojoin,omitempty"`
c189ae5… noreply 283
c189ae5… noreply 284 // Modes is a list of channel modes to set after provisioning (e.g. "+m" for moderated).
c189ae5… noreply 285 Modes []string `yaml:"modes" json:"modes,omitempty"`
6d94dfd… noreply 286
6d94dfd… noreply 287 // OnJoinMessage is sent to agents when they join this channel.
6d94dfd… noreply 288 // Supports template variables: {nick}, {channel}.
6d94dfd… noreply 289 OnJoinMessage string `yaml:"on_join_message" json:"on_join_message,omitempty"`
5705614… lmata 290 }
5705614… lmata 291
5705614… lmata 292 // ChannelTypeConfig defines policy rules for a class of dynamically created channels.
5705614… lmata 293 // Matched by prefix against channel names (e.g. prefix "task." matches "#task.gh-42").
5705614… lmata 294 type ChannelTypeConfig struct {
5705614… lmata 295 // Name is a human-readable type identifier (e.g. "task", "sprint", "incident").
0619d47… lmata 296 Name string `yaml:"name" json:"name"`
5705614… lmata 297
5705614… lmata 298 // Prefix is matched against channel names after stripping the leading #.
5705614… lmata 299 // The first matching type wins. (e.g. "task." matches "#task.gh-42")
0619d47… lmata 300 Prefix string `yaml:"prefix" json:"prefix"`
5705614… lmata 301
5705614… lmata 302 // Autojoin is a list of bot nicks to invite when a channel of this type is created.
0619d47… lmata 303 Autojoin []string `yaml:"autojoin" json:"autojoin,omitempty"`
5705614… lmata 304
5705614… lmata 305 // Supervision is the coordination channel where summaries should surface.
0619d47… lmata 306 Supervision string `yaml:"supervision" json:"supervision,omitempty"`
0619d47… lmata 307
c189ae5… noreply 308 // Modes is a list of channel modes to set when provisioning (e.g. "+m" for moderated).
c189ae5… noreply 309 Modes []string `yaml:"modes" json:"modes,omitempty"`
c189ae5… noreply 310
5705614… lmata 311 // Ephemeral marks channels of this type for automatic cleanup.
0619d47… lmata 312 Ephemeral bool `yaml:"ephemeral" json:"ephemeral,omitempty"`
5705614… lmata 313
5705614… lmata 314 // TTL is the maximum lifetime of an ephemeral channel with no non-bot members.
5705614… lmata 315 // Zero means no TTL; cleanup only occurs when the channel is empty.
0619d47… lmata 316 TTL Duration `yaml:"ttl" json:"ttl,omitempty"`
6d94dfd… noreply 317
6d94dfd… noreply 318 // OnJoinMessage is sent to agents when they join a channel of this type.
6d94dfd… noreply 319 OnJoinMessage string `yaml:"on_join_message" json:"on_join_message,omitempty"`
5705614… lmata 320 }
5705614… lmata 321
0619d47… lmata 322 // Duration wraps time.Duration for YAML/JSON marshalling ("72h", "30m", etc.).
5705614… lmata 323 type Duration struct {
5705614… lmata 324 time.Duration
0619d47… lmata 325 }
0619d47… lmata 326
0619d47… lmata 327 func (d Duration) MarshalJSON() ([]byte, error) {
0619d47… lmata 328 if d.Duration == 0 {
0619d47… lmata 329 return []byte(`"0s"`), nil
0619d47… lmata 330 }
0619d47… lmata 331 return []byte(`"` + d.Duration.String() + `"`), nil
0619d47… lmata 332 }
0619d47… lmata 333
0619d47… lmata 334 func (d *Duration) UnmarshalJSON(b []byte) error {
0619d47… lmata 335 s := string(b)
0619d47… lmata 336 if len(s) < 2 || s[0] != '"' || s[len(s)-1] != '"' {
0619d47… lmata 337 return fmt.Errorf("config: duration must be a quoted string, got %s", s)
0619d47… lmata 338 }
0619d47… lmata 339 dur, err := time.ParseDuration(s[1 : len(s)-1])
0619d47… lmata 340 if err != nil {
0619d47… lmata 341 return fmt.Errorf("config: invalid duration %s: %w", s, err)
0619d47… lmata 342 }
0619d47… lmata 343 d.Duration = dur
0619d47… lmata 344 return nil
5705614… lmata 345 }
5705614… lmata 346
5705614… lmata 347 func (d *Duration) UnmarshalYAML(value *yaml.Node) error {
5705614… lmata 348 var s string
5705614… lmata 349 if err := value.Decode(&s); err != nil {
5705614… lmata 350 return err
5705614… lmata 351 }
5705614… lmata 352 dur, err := time.ParseDuration(s)
5705614… lmata 353 if err != nil {
5705614… lmata 354 return fmt.Errorf("config: invalid duration %q: %w", s, err)
5705614… lmata 355 }
5705614… lmata 356 d.Duration = dur
17e2c1d… lmata 357 return nil
17e2c1d… lmata 358 }
17e2c1d… lmata 359
17e2c1d… lmata 360 // MarshalYAML encodes Duration as a human-readable string ("72h", "30m").
17e2c1d… lmata 361 func (d Duration) MarshalYAML() (any, error) {
17e2c1d… lmata 362 if d.Duration == 0 {
17e2c1d… lmata 363 return "0s", nil
17e2c1d… lmata 364 }
17e2c1d… lmata 365 return d.Duration.String(), nil
17e2c1d… lmata 366 }
17e2c1d… lmata 367
17e2c1d… lmata 368 // Save marshals c to YAML and writes it to path atomically (write to a temp
17e2c1d… lmata 369 // file in the same directory, then rename). Comments in the original file are
17e2c1d… lmata 370 // not preserved after the first save.
17e2c1d… lmata 371 func (c *Config) Save(path string) error {
17e2c1d… lmata 372 data, err := yaml.Marshal(c)
17e2c1d… lmata 373 if err != nil {
17e2c1d… lmata 374 return fmt.Errorf("config: marshal: %w", err)
17e2c1d… lmata 375 }
17e2c1d… lmata 376 // Write to a sibling temp file then rename for atomic replacement.
17e2c1d… lmata 377 tmp := path + ".tmp"
17e2c1d… lmata 378 if err := os.WriteFile(tmp, data, 0o600); err != nil {
17e2c1d… lmata 379 return fmt.Errorf("config: write %s: %w", tmp, err)
17e2c1d… lmata 380 }
17e2c1d… lmata 381 if err := os.Rename(tmp, path); err != nil {
17e2c1d… lmata 382 _ = os.Remove(tmp)
17e2c1d… lmata 383 return fmt.Errorf("config: rename %s → %s: %w", tmp, path, err)
17e2c1d… lmata 384 }
5705614… lmata 385 return nil
5705614… lmata 386 }
5705614… lmata 387
c369cd5… lmata 388 // Defaults fills in zero values with sensible defaults.
c369cd5… lmata 389 func (c *Config) Defaults() {
c369cd5… lmata 390 if c.Ergo.BinaryPath == "" {
c369cd5… lmata 391 c.Ergo.BinaryPath = "ergo"
c369cd5… lmata 392 }
c369cd5… lmata 393 if c.Ergo.DataDir == "" {
c369cd5… lmata 394 c.Ergo.DataDir = "./data/ergo"
c369cd5… lmata 395 }
c369cd5… lmata 396 if c.Ergo.NetworkName == "" {
c369cd5… lmata 397 c.Ergo.NetworkName = "scuttlebot"
c369cd5… lmata 398 }
c369cd5… lmata 399 if c.Ergo.ServerName == "" {
c369cd5… lmata 400 c.Ergo.ServerName = "irc.scuttlebot.local"
c369cd5… lmata 401 }
c369cd5… lmata 402 if c.Ergo.IRCAddr == "" {
c369cd5… lmata 403 c.Ergo.IRCAddr = "127.0.0.1:6667"
18e8fef… lmata 404 }
18e8fef… lmata 405 if c.Ergo.TLSDomain != "" && c.Ergo.TLSAddr == "" {
18e8fef… lmata 406 c.Ergo.TLSAddr = "0.0.0.0:6697"
0e78954… lmata 407 }
c369cd5… lmata 408 if c.Ergo.APIAddr == "" {
c369cd5… lmata 409 c.Ergo.APIAddr = "127.0.0.1:8089"
c369cd5… lmata 410 }
c369cd5… lmata 411 if c.Datastore.Driver == "" {
c369cd5… lmata 412 c.Datastore.Driver = "sqlite"
c369cd5… lmata 413 }
c369cd5… lmata 414 if c.Datastore.DSN == "" {
c369cd5… lmata 415 c.Datastore.DSN = "./data/scuttlebot.db"
2d8a379… lmata 416 }
2d8a379… lmata 417 if c.APIAddr == "" {
0e78954… lmata 418 c.APIAddr = "127.0.0.1:8080"
550b35e… lmata 419 }
550b35e… lmata 420 if c.MCPAddr == "" {
0e78954… lmata 421 c.MCPAddr = "127.0.0.1:8081"
0e78954… lmata 422 }
0e78954… lmata 423 if c.Ergo.DefaultChannelModes == "" {
0e78954… lmata 424 c.Ergo.DefaultChannelModes = "+n"
d74d207… lmata 425 }
d74d207… lmata 426 if !c.Bridge.Enabled && c.Bridge.Nick == "" {
d74d207… lmata 427 c.Bridge.Enabled = true // enabled by default
5ac549c… lmata 428 }
5ac549c… lmata 429 if c.TLS.Domain != "" && !c.TLS.AllowInsecure {
5ac549c… lmata 430 c.TLS.AllowInsecure = true // HTTP always on by default
d74d207… lmata 431 }
d74d207… lmata 432 if c.Bridge.Nick == "" {
d74d207… lmata 433 c.Bridge.Nick = "bridge"
d74d207… lmata 434 }
d74d207… lmata 435 if c.Bridge.BufferSize == 0 {
d74d207… lmata 436 c.Bridge.BufferSize = 200
5ac549c… lmata 437 }
5ac549c… lmata 438 if c.Bridge.WebUserTTLMinutes == 0 {
5ac549c… lmata 439 c.Bridge.WebUserTTLMinutes = 5
d6520d1… lmata 440 }
d6520d1… lmata 441 if c.Topology.Nick == "" {
d6520d1… lmata 442 c.Topology.Nick = "topology"
d6520d1… lmata 443 }
17e2c1d… lmata 444 if c.History.Keep == 0 {
17e2c1d… lmata 445 c.History.Keep = 20
17e2c1d… lmata 446 }
2d8a379… lmata 447 }
2d8a379… lmata 448
2d8a379… lmata 449 func envStr(key string) string { return os.Getenv(key) }
61c045e… lmata 450
61c045e… lmata 451 // LoadFile reads a YAML config file into c. Missing file is not an error —
61c045e… lmata 452 // returns nil so callers can treat an absent config file as "use defaults".
61c045e… lmata 453 // Call Defaults() first, then LoadFile(), then ApplyEnv() so that file values
61c045e… lmata 454 // override defaults and env values override the file.
61c045e… lmata 455 func (c *Config) LoadFile(path string) error {
61c045e… lmata 456 data, err := os.ReadFile(path)
61c045e… lmata 457 if os.IsNotExist(err) {
61c045e… lmata 458 return nil
61c045e… lmata 459 }
61c045e… lmata 460 if err != nil {
61c045e… lmata 461 return fmt.Errorf("config: read %s: %w", path, err)
61c045e… lmata 462 }
17e2c1d… lmata 463 return c.LoadFromBytes(data)
17e2c1d… lmata 464 }
17e2c1d… lmata 465
17e2c1d… lmata 466 // LoadFromBytes parses YAML config bytes into c.
17e2c1d… lmata 467 func (c *Config) LoadFromBytes(data []byte) error {
61c045e… lmata 468 if err := yaml.Unmarshal(data, c); err != nil {
17e2c1d… lmata 469 return fmt.Errorf("config: parse: %w", err)
61c045e… lmata 470 }
61c045e… lmata 471 return nil
61c045e… lmata 472 }
2d8a379… lmata 473
2d8a379… lmata 474 // ApplyEnv overrides config values with SCUTTLEBOT_* environment variables.
2d8a379… lmata 475 // Call after Defaults() to allow env to override defaults.
2d8a379… lmata 476 //
2d8a379… lmata 477 // Supported variables:
2d8a379… lmata 478 //
2d8a379… lmata 479 // SCUTTLEBOT_API_ADDR — scuttlebot HTTP API listen address (e.g. ":8080")
2d8a379… lmata 480 // SCUTTLEBOT_DB_DRIVER — "sqlite" or "postgres"
2d8a379… lmata 481 // SCUTTLEBOT_DB_DSN — datastore connection string
2d8a379… lmata 482 // SCUTTLEBOT_ERGO_EXTERNAL — "true" to skip subprocess management
2d8a379… lmata 483 // SCUTTLEBOT_ERGO_API_ADDR — ergo HTTP API address (e.g. "http://ergo:8089")
2d8a379… lmata 484 // SCUTTLEBOT_ERGO_API_TOKEN — ergo HTTP API bearer token
2d8a379… lmata 485 // SCUTTLEBOT_ERGO_IRC_ADDR — ergo IRC listen/connect address (e.g. "ergo:6667")
2d8a379… lmata 486 // SCUTTLEBOT_ERGO_NETWORK_NAME — IRC network name
2d8a379… lmata 487 // SCUTTLEBOT_ERGO_SERVER_NAME — IRC server hostname
2d8a379… lmata 488 func (c *Config) ApplyEnv() {
2d8a379… lmata 489 if v := envStr("SCUTTLEBOT_API_ADDR"); v != "" {
2d8a379… lmata 490 c.APIAddr = v
2d8a379… lmata 491 }
2d8a379… lmata 492 if v := envStr("SCUTTLEBOT_DB_DRIVER"); v != "" {
2d8a379… lmata 493 c.Datastore.Driver = v
2d8a379… lmata 494 }
2d8a379… lmata 495 if v := envStr("SCUTTLEBOT_DB_DSN"); v != "" {
2d8a379… lmata 496 c.Datastore.DSN = v
2d8a379… lmata 497 }
2d8a379… lmata 498 if v := envStr("SCUTTLEBOT_ERGO_EXTERNAL"); v == "true" || v == "1" {
2d8a379… lmata 499 c.Ergo.External = true
2d8a379… lmata 500 }
2d8a379… lmata 501 if v := envStr("SCUTTLEBOT_ERGO_API_ADDR"); v != "" {
2d8a379… lmata 502 c.Ergo.APIAddr = v
2d8a379… lmata 503 }
2d8a379… lmata 504 if v := envStr("SCUTTLEBOT_ERGO_API_TOKEN"); v != "" {
2d8a379… lmata 505 c.Ergo.APIToken = v
2d8a379… lmata 506 }
2d8a379… lmata 507 if v := envStr("SCUTTLEBOT_ERGO_IRC_ADDR"); v != "" {
2d8a379… lmata 508 c.Ergo.IRCAddr = v
2d8a379… lmata 509 }
2d8a379… lmata 510 if v := envStr("SCUTTLEBOT_ERGO_NETWORK_NAME"); v != "" {
2d8a379… lmata 511 c.Ergo.NetworkName = v
2d8a379… lmata 512 }
2d8a379… lmata 513 if v := envStr("SCUTTLEBOT_ERGO_SERVER_NAME"); v != "" {
2d8a379… lmata 514 c.Ergo.ServerName = v
550b35e… lmata 515 }
550b35e… lmata 516 if v := envStr("SCUTTLEBOT_MCP_ADDR"); v != "" {
550b35e… lmata 517 c.MCPAddr = v
c369cd5… lmata 518 }
c369cd5… lmata 519 }

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button