ScuttleBot
fix(config): add json tags to topology/history structs and Duration JSON marshalling GET /v1/config was returning capitalized Go field names for TopologyConfig and ConfigHistoryConfig. Added json struct tags throughout, plus MarshalJSON and UnmarshalJSON on Duration so TTL values round-trip as quoted strings ("72h").
Commit
0619d47c699e5d9d2250da8486bb4363cf6b426e06d11a1dab74f89e07419165
Parent
17e2c1d1e649742…
1 file changed
+37
-20
+37
-20
| --- internal/config/config.go | ||
| +++ internal/config/config.go | ||
| @@ -31,15 +31,15 @@ | ||
| 31 | 31 | |
| 32 | 32 | // ConfigHistoryConfig controls config write-back history retention. |
| 33 | 33 | type ConfigHistoryConfig struct { |
| 34 | 34 | // Keep is the number of config snapshots to retain in Dir. |
| 35 | 35 | // 0 disables history. Default: 20. |
| 36 | - Keep int `yaml:"keep"` | |
| 36 | + Keep int `yaml:"keep" json:"keep"` | |
| 37 | 37 | |
| 38 | 38 | // Dir is the directory for config snapshots. |
| 39 | 39 | // Default: {ergo.data_dir}/config-history |
| 40 | - Dir string `yaml:"dir"` | |
| 40 | + Dir string `yaml:"dir" json:"dir,omitempty"` | |
| 41 | 41 | } |
| 42 | 42 | |
| 43 | 43 | // LLMConfig configures the omnibus LLM gateway used by oracle and any other |
| 44 | 44 | // bot or service that needs language model access. |
| 45 | 45 | type LLMConfig struct { |
| @@ -210,69 +210,86 @@ | ||
| 210 | 210 | // It defines static channels provisioned at startup and dynamic channel type |
| 211 | 211 | // rules applied when agents create channels at runtime. |
| 212 | 212 | type TopologyConfig struct { |
| 213 | 213 | // Nick is the IRC nick used by the topology manager to provision channels |
| 214 | 214 | // via ChanServ. Defaults to "topology". |
| 215 | - Nick string `yaml:"nick"` | |
| 215 | + Nick string `yaml:"nick" json:"nick"` | |
| 216 | 216 | |
| 217 | 217 | // Channels are static channels provisioned at daemon startup. |
| 218 | - Channels []StaticChannelConfig `yaml:"channels"` | |
| 218 | + Channels []StaticChannelConfig `yaml:"channels" json:"channels"` | |
| 219 | 219 | |
| 220 | 220 | // Types are prefix-based rules applied to dynamically created channels. |
| 221 | 221 | // The first matching prefix wins. |
| 222 | - Types []ChannelTypeConfig `yaml:"types"` | |
| 222 | + Types []ChannelTypeConfig `yaml:"types" json:"types"` | |
| 223 | 223 | } |
| 224 | 224 | |
| 225 | 225 | // StaticChannelConfig describes a channel that is provisioned at startup. |
| 226 | 226 | type StaticChannelConfig struct { |
| 227 | 227 | // Name is the full channel name including the # prefix (e.g. "#general"). |
| 228 | - Name string `yaml:"name"` | |
| 228 | + Name string `yaml:"name" json:"name"` | |
| 229 | 229 | |
| 230 | 230 | // Topic is the initial channel topic. |
| 231 | - Topic string `yaml:"topic"` | |
| 231 | + Topic string `yaml:"topic" json:"topic,omitempty"` | |
| 232 | 232 | |
| 233 | 233 | // Ops is a list of nicks to grant channel operator (+o) access. |
| 234 | - Ops []string `yaml:"ops"` | |
| 234 | + Ops []string `yaml:"ops" json:"ops,omitempty"` | |
| 235 | 235 | |
| 236 | 236 | // Voice is a list of nicks to grant voice (+v) access. |
| 237 | - Voice []string `yaml:"voice"` | |
| 237 | + Voice []string `yaml:"voice" json:"voice,omitempty"` | |
| 238 | 238 | |
| 239 | 239 | // Autojoin is a list of bot nicks to invite when the channel is provisioned. |
| 240 | - Autojoin []string `yaml:"autojoin"` | |
| 240 | + Autojoin []string `yaml:"autojoin" json:"autojoin,omitempty"` | |
| 241 | 241 | } |
| 242 | 242 | |
| 243 | 243 | // ChannelTypeConfig defines policy rules for a class of dynamically created channels. |
| 244 | 244 | // Matched by prefix against channel names (e.g. prefix "task." matches "#task.gh-42"). |
| 245 | 245 | type ChannelTypeConfig struct { |
| 246 | 246 | // Name is a human-readable type identifier (e.g. "task", "sprint", "incident"). |
| 247 | - Name string `yaml:"name"` | |
| 247 | + Name string `yaml:"name" json:"name"` | |
| 248 | 248 | |
| 249 | 249 | // Prefix is matched against channel names after stripping the leading #. |
| 250 | 250 | // The first matching type wins. (e.g. "task." matches "#task.gh-42") |
| 251 | - Prefix string `yaml:"prefix"` | |
| 251 | + Prefix string `yaml:"prefix" json:"prefix"` | |
| 252 | 252 | |
| 253 | 253 | // Autojoin is a list of bot nicks to invite when a channel of this type is created. |
| 254 | - Autojoin []string `yaml:"autojoin"` | |
| 254 | + Autojoin []string `yaml:"autojoin" json:"autojoin,omitempty"` | |
| 255 | 255 | |
| 256 | 256 | // Supervision is the coordination channel where summaries should surface. |
| 257 | - // Agents receive this when they create a channel so they know where to also post. | |
| 258 | - // May be a static channel name (e.g. "#general") or a type prefix pattern | |
| 259 | - // (e.g. "sprint." — resolved to the most recently created matching channel). | |
| 260 | - Supervision string `yaml:"supervision"` | |
| 257 | + Supervision string `yaml:"supervision" json:"supervision,omitempty"` | |
| 261 | 258 | |
| 262 | 259 | // Ephemeral marks channels of this type for automatic cleanup. |
| 263 | - Ephemeral bool `yaml:"ephemeral"` | |
| 260 | + Ephemeral bool `yaml:"ephemeral" json:"ephemeral,omitempty"` | |
| 264 | 261 | |
| 265 | 262 | // TTL is the maximum lifetime of an ephemeral channel with no non-bot members. |
| 266 | 263 | // Zero means no TTL; cleanup only occurs when the channel is empty. |
| 267 | - TTL Duration `yaml:"ttl"` | |
| 264 | + TTL Duration `yaml:"ttl" json:"ttl,omitempty"` | |
| 268 | 265 | } |
| 269 | 266 | |
| 270 | -// Duration wraps time.Duration for YAML unmarshalling ("72h", "30m", etc.). | |
| 267 | +// Duration wraps time.Duration for YAML/JSON marshalling ("72h", "30m", etc.). | |
| 271 | 268 | type Duration struct { |
| 272 | 269 | time.Duration |
| 273 | 270 | } |
| 271 | + | |
| 272 | +func (d Duration) MarshalJSON() ([]byte, error) { | |
| 273 | + if d.Duration == 0 { | |
| 274 | + return []byte(`"0s"`), nil | |
| 275 | + } | |
| 276 | + return []byte(`"` + d.Duration.String() + `"`), nil | |
| 277 | +} | |
| 278 | + | |
| 279 | +func (d *Duration) UnmarshalJSON(b []byte) error { | |
| 280 | + s := string(b) | |
| 281 | + if len(s) < 2 || s[0] != '"' || s[len(s)-1] != '"' { | |
| 282 | + return fmt.Errorf("config: duration must be a quoted string, got %s", s) | |
| 283 | + } | |
| 284 | + dur, err := time.ParseDuration(s[1 : len(s)-1]) | |
| 285 | + if err != nil { | |
| 286 | + return fmt.Errorf("config: invalid duration %s: %w", s, err) | |
| 287 | + } | |
| 288 | + d.Duration = dur | |
| 289 | + return nil | |
| 290 | +} | |
| 274 | 291 | |
| 275 | 292 | func (d *Duration) UnmarshalYAML(value *yaml.Node) error { |
| 276 | 293 | var s string |
| 277 | 294 | if err := value.Decode(&s); err != nil { |
| 278 | 295 | return err |
| 279 | 296 |
| --- internal/config/config.go | |
| +++ internal/config/config.go | |
| @@ -31,15 +31,15 @@ | |
| 31 | |
| 32 | // ConfigHistoryConfig controls config write-back history retention. |
| 33 | type ConfigHistoryConfig struct { |
| 34 | // Keep is the number of config snapshots to retain in Dir. |
| 35 | // 0 disables history. Default: 20. |
| 36 | Keep int `yaml:"keep"` |
| 37 | |
| 38 | // Dir is the directory for config snapshots. |
| 39 | // Default: {ergo.data_dir}/config-history |
| 40 | Dir string `yaml:"dir"` |
| 41 | } |
| 42 | |
| 43 | // LLMConfig configures the omnibus LLM gateway used by oracle and any other |
| 44 | // bot or service that needs language model access. |
| 45 | type LLMConfig struct { |
| @@ -210,69 +210,86 @@ | |
| 210 | // It defines static channels provisioned at startup and dynamic channel type |
| 211 | // rules applied when agents create channels at runtime. |
| 212 | type TopologyConfig struct { |
| 213 | // Nick is the IRC nick used by the topology manager to provision channels |
| 214 | // via ChanServ. Defaults to "topology". |
| 215 | Nick string `yaml:"nick"` |
| 216 | |
| 217 | // Channels are static channels provisioned at daemon startup. |
| 218 | Channels []StaticChannelConfig `yaml:"channels"` |
| 219 | |
| 220 | // Types are prefix-based rules applied to dynamically created channels. |
| 221 | // The first matching prefix wins. |
| 222 | Types []ChannelTypeConfig `yaml:"types"` |
| 223 | } |
| 224 | |
| 225 | // StaticChannelConfig describes a channel that is provisioned at startup. |
| 226 | type StaticChannelConfig struct { |
| 227 | // Name is the full channel name including the # prefix (e.g. "#general"). |
| 228 | Name string `yaml:"name"` |
| 229 | |
| 230 | // Topic is the initial channel topic. |
| 231 | Topic string `yaml:"topic"` |
| 232 | |
| 233 | // Ops is a list of nicks to grant channel operator (+o) access. |
| 234 | Ops []string `yaml:"ops"` |
| 235 | |
| 236 | // Voice is a list of nicks to grant voice (+v) access. |
| 237 | Voice []string `yaml:"voice"` |
| 238 | |
| 239 | // Autojoin is a list of bot nicks to invite when the channel is provisioned. |
| 240 | Autojoin []string `yaml:"autojoin"` |
| 241 | } |
| 242 | |
| 243 | // ChannelTypeConfig defines policy rules for a class of dynamically created channels. |
| 244 | // Matched by prefix against channel names (e.g. prefix "task." matches "#task.gh-42"). |
| 245 | type ChannelTypeConfig struct { |
| 246 | // Name is a human-readable type identifier (e.g. "task", "sprint", "incident"). |
| 247 | Name string `yaml:"name"` |
| 248 | |
| 249 | // Prefix is matched against channel names after stripping the leading #. |
| 250 | // The first matching type wins. (e.g. "task." matches "#task.gh-42") |
| 251 | Prefix string `yaml:"prefix"` |
| 252 | |
| 253 | // Autojoin is a list of bot nicks to invite when a channel of this type is created. |
| 254 | Autojoin []string `yaml:"autojoin"` |
| 255 | |
| 256 | // Supervision is the coordination channel where summaries should surface. |
| 257 | // Agents receive this when they create a channel so they know where to also post. |
| 258 | // May be a static channel name (e.g. "#general") or a type prefix pattern |
| 259 | // (e.g. "sprint." — resolved to the most recently created matching channel). |
| 260 | Supervision string `yaml:"supervision"` |
| 261 | |
| 262 | // Ephemeral marks channels of this type for automatic cleanup. |
| 263 | Ephemeral bool `yaml:"ephemeral"` |
| 264 | |
| 265 | // TTL is the maximum lifetime of an ephemeral channel with no non-bot members. |
| 266 | // Zero means no TTL; cleanup only occurs when the channel is empty. |
| 267 | TTL Duration `yaml:"ttl"` |
| 268 | } |
| 269 | |
| 270 | // Duration wraps time.Duration for YAML unmarshalling ("72h", "30m", etc.). |
| 271 | type Duration struct { |
| 272 | time.Duration |
| 273 | } |
| 274 | |
| 275 | func (d *Duration) UnmarshalYAML(value *yaml.Node) error { |
| 276 | var s string |
| 277 | if err := value.Decode(&s); err != nil { |
| 278 | return err |
| 279 |
| --- internal/config/config.go | |
| +++ internal/config/config.go | |
| @@ -31,15 +31,15 @@ | |
| 31 | |
| 32 | // ConfigHistoryConfig controls config write-back history retention. |
| 33 | type ConfigHistoryConfig struct { |
| 34 | // Keep is the number of config snapshots to retain in Dir. |
| 35 | // 0 disables history. Default: 20. |
| 36 | Keep int `yaml:"keep" json:"keep"` |
| 37 | |
| 38 | // Dir is the directory for config snapshots. |
| 39 | // Default: {ergo.data_dir}/config-history |
| 40 | Dir string `yaml:"dir" json:"dir,omitempty"` |
| 41 | } |
| 42 | |
| 43 | // LLMConfig configures the omnibus LLM gateway used by oracle and any other |
| 44 | // bot or service that needs language model access. |
| 45 | type LLMConfig struct { |
| @@ -210,69 +210,86 @@ | |
| 210 | // It defines static channels provisioned at startup and dynamic channel type |
| 211 | // rules applied when agents create channels at runtime. |
| 212 | type TopologyConfig struct { |
| 213 | // Nick is the IRC nick used by the topology manager to provision channels |
| 214 | // via ChanServ. Defaults to "topology". |
| 215 | Nick string `yaml:"nick" json:"nick"` |
| 216 | |
| 217 | // Channels are static channels provisioned at daemon startup. |
| 218 | Channels []StaticChannelConfig `yaml:"channels" json:"channels"` |
| 219 | |
| 220 | // Types are prefix-based rules applied to dynamically created channels. |
| 221 | // The first matching prefix wins. |
| 222 | Types []ChannelTypeConfig `yaml:"types" json:"types"` |
| 223 | } |
| 224 | |
| 225 | // StaticChannelConfig describes a channel that is provisioned at startup. |
| 226 | type StaticChannelConfig struct { |
| 227 | // Name is the full channel name including the # prefix (e.g. "#general"). |
| 228 | Name string `yaml:"name" json:"name"` |
| 229 | |
| 230 | // Topic is the initial channel topic. |
| 231 | Topic string `yaml:"topic" json:"topic,omitempty"` |
| 232 | |
| 233 | // Ops is a list of nicks to grant channel operator (+o) access. |
| 234 | Ops []string `yaml:"ops" json:"ops,omitempty"` |
| 235 | |
| 236 | // Voice is a list of nicks to grant voice (+v) access. |
| 237 | Voice []string `yaml:"voice" json:"voice,omitempty"` |
| 238 | |
| 239 | // Autojoin is a list of bot nicks to invite when the channel is provisioned. |
| 240 | Autojoin []string `yaml:"autojoin" json:"autojoin,omitempty"` |
| 241 | } |
| 242 | |
| 243 | // ChannelTypeConfig defines policy rules for a class of dynamically created channels. |
| 244 | // Matched by prefix against channel names (e.g. prefix "task." matches "#task.gh-42"). |
| 245 | type ChannelTypeConfig struct { |
| 246 | // Name is a human-readable type identifier (e.g. "task", "sprint", "incident"). |
| 247 | Name string `yaml:"name" json:"name"` |
| 248 | |
| 249 | // Prefix is matched against channel names after stripping the leading #. |
| 250 | // The first matching type wins. (e.g. "task." matches "#task.gh-42") |
| 251 | Prefix string `yaml:"prefix" json:"prefix"` |
| 252 | |
| 253 | // Autojoin is a list of bot nicks to invite when a channel of this type is created. |
| 254 | Autojoin []string `yaml:"autojoin" json:"autojoin,omitempty"` |
| 255 | |
| 256 | // Supervision is the coordination channel where summaries should surface. |
| 257 | Supervision string `yaml:"supervision" json:"supervision,omitempty"` |
| 258 | |
| 259 | // Ephemeral marks channels of this type for automatic cleanup. |
| 260 | Ephemeral bool `yaml:"ephemeral" json:"ephemeral,omitempty"` |
| 261 | |
| 262 | // TTL is the maximum lifetime of an ephemeral channel with no non-bot members. |
| 263 | // Zero means no TTL; cleanup only occurs when the channel is empty. |
| 264 | TTL Duration `yaml:"ttl" json:"ttl,omitempty"` |
| 265 | } |
| 266 | |
| 267 | // Duration wraps time.Duration for YAML/JSON marshalling ("72h", "30m", etc.). |
| 268 | type Duration struct { |
| 269 | time.Duration |
| 270 | } |
| 271 | |
| 272 | func (d Duration) MarshalJSON() ([]byte, error) { |
| 273 | if d.Duration == 0 { |
| 274 | return []byte(`"0s"`), nil |
| 275 | } |
| 276 | return []byte(`"` + d.Duration.String() + `"`), nil |
| 277 | } |
| 278 | |
| 279 | func (d *Duration) UnmarshalJSON(b []byte) error { |
| 280 | s := string(b) |
| 281 | if len(s) < 2 || s[0] != '"' || s[len(s)-1] != '"' { |
| 282 | return fmt.Errorf("config: duration must be a quoted string, got %s", s) |
| 283 | } |
| 284 | dur, err := time.ParseDuration(s[1 : len(s)-1]) |
| 285 | if err != nil { |
| 286 | return fmt.Errorf("config: invalid duration %s: %w", s, err) |
| 287 | } |
| 288 | d.Duration = dur |
| 289 | return nil |
| 290 | } |
| 291 | |
| 292 | func (d *Duration) UnmarshalYAML(value *yaml.Node) error { |
| 293 | var s string |
| 294 | if err := value.Decode(&s); err != nil { |
| 295 | return err |
| 296 |