{"record":{"id":"1733b811782b20f5","repo":"sipeed/picoclaw","slug":"failed-to-merge-channels-from-security-config-w","errorCode":null,"errorMessage":"failed to merge channels from security config: %w","messagePattern":"failed to merge channels from security config: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pkg/config/security.go","lineNumber":93,"sourceCode":"\t// Unmarshal non-channel fields from security.yml\n\t// This will resolve encrypted values for model_list, tools, etc.\n\tif err := yaml.Unmarshal(data, cfg); err != nil {\n\t\treturn fmt.Errorf(\"failed to parse security config %s: %w\", securityPath, err)\n\t}\n\tif err := applyLegacySkillsSecurityConfig(cfg, data); err != nil {\n\t\treturn fmt.Errorf(\"failed to parse legacy skills security config: %w\", err)\n\t}\n\n\t// Restore channels from saved, then manually merge from security.yml\n\tcfg.Channels = make(ChannelsConfig)\n\tfor name, savedBC := range savedChannels {\n\t\tcfg.Channels[name] = savedBC\n\t}\n\n\t// If we found a channels node in security.yml, merge it into existing channels\n\tif channelsNode != nil {\n\t\tif err := cfg.Channels.UnmarshalYAML(channelsNode); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to merge channels from security config: %w\", err)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc applyLegacySkillsSecurityConfig(cfg *Config, data []byte) error {\n\tvar root yaml.Node\n\tif err := yaml.Unmarshal(data, &root); err != nil {\n\t\treturn err\n\t}\n\tif len(root.Content) == 0 {\n\t\treturn nil\n\t}\n\n\trootMap := root.Content[0]\n\tif rootMap == nil || rootMap.Kind != yaml.MappingNode {\n\t\treturn nil","sourceCodeStart":75,"sourceCodeEnd":111,"githubUrl":"https://github.com/sipeed/picoclaw/blob/49183d7e8daed0dba89ddbb6fcb60089401d9680/pkg/config/security.go#L75-L111","documentation":"Returned by loadSecurityConfig when security.yml contains a channels/channel_list node and cfg.Channels.UnmarshalYAML(channelsNode) rejects it. Config.Channels has a custom YAML unmarshaler; it fails when a channel entry under `channels:` (or legacy `channel_list:`) has an invalid type or shape — e.g. a channel value that is not a map, or a nested field the channel decoder refuses.","triggerScenarios":"security.yml has `channels: { slack: \"just-a-string\" }` (channel value not a map), an unknown nested structure inside a channel, or a channel_list entry whose elements don't fit the ChannelsConfig decode. The custom UnmarshalYAML's error is wrapped with %w.","commonSituations":"Moving channel config into security.yml and getting the nesting wrong (channel name mapped to a scalar instead of its settings map), or version drift where a channel's expected fields changed. Note the code accepts both `channels` and `channel_list` keys, so a legacy `channel_list` with new-format contents also triggers it.","solutions":["Read the wrapped error for the offending channel name/field; make each channel value a proper map of its settings","Check the exact layout: `channels:` -> channel name -> settings map (not a scalar, not a bare list)","If you kept the legacy `channel_list` key, verify its entries still match the legacy shape or migrate them to `channels`","Reload and confirm Channels merges cleanly"],"exampleFix":"# before\nchannels:\n  slack: slack-token-value\n\n# after\nchannels:\n  slack:\n    token: slack-token-value","handlingStrategy":"validation","validationCode":"// Structural check: every channels value must be a map.\nfunc channelsShapeOK(node *yaml.Node) error {\n\tfor i, n := range node.Content {\n\t\t_ = i\n\t\tif n.Tag == \"!!map\" { continue }\n\t\treturn fmt.Errorf(\"channel value is %s, want map\", n.Tag)\n\t}\n\treturn nil\n}","typeGuard":"func isChannelMap(v any) bool {\n\t_, ok := v.(map[string]any)\n\treturn ok\n}","tryCatchPattern":"if err := loadSecurityConfig(cfg, p); err != nil {\n\tif strings.Contains(err.Error(), \"merge channels\") {\n\t\t// inspect security.yml channels/channel_list nesting\n\t}\n\treturn err\n}","preventionTips":["Keep channel name -> settings-map nesting in security.yml","Remember both `channels` and legacy `channel_list` keys are honored — use only the current one","Copy channel blocks verbatim from working configs when moving them"],"tags":["go","config","yaml","channels","security"],"backgroundTag":null,"analyzedSha":"49183d7e8daed0dba89ddbb6fcb60089401d9680","analyzedAt":"2026-08-15T21:55:41.315Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}