fix skills registry config merge compatibility

This commit is contained in:
lxowalle 2026-04-10 16:29:07 +08:00
parent 528bb66750
commit 70c200e4d3
3 changed files with 220 additions and 43 deletions

View file

@ -334,20 +334,41 @@ func (v *SkillsRegistriesConfig) UnmarshalJSON(data []byte) error {
return nil
}
legacy := map[string]*SkillRegistryConfig{}
legacy := map[string]json.RawMessage{}
if err := json.Unmarshal(data, &legacy); err != nil {
return err
}
list = make([]*SkillRegistryConfig, 0, len(legacy))
for name, registry := range legacy {
if len(*v) == 0 {
keys := make([]string, 0, len(legacy))
for name := range legacy {
keys = append(keys, name)
}
sort.Strings(keys)
list = make([]*SkillRegistryConfig, 0, len(keys))
for _, name := range keys {
var registry SkillRegistryConfig
if err := json.Unmarshal(legacy[name], &registry); err != nil {
return err
}
registry.Name = name
list = append(list, &registry)
}
*v = list
return nil
}
for _, name := range sortedRegistryNamesFromJSON(legacy) {
registry := cloneRegistryConfig(findRegistryConfigByName(*v, name))
if registry == nil {
continue
registry = &SkillRegistryConfig{Name: name}
}
if err := json.Unmarshal(legacy[name], registry); err != nil {
return err
}
registry.Name = name
list = append(list, registry)
v.Set(name, *registry)
}
*v = list
return nil
}
@ -366,24 +387,42 @@ func (v SkillsRegistriesConfig) MarshalJSON() ([]byte, error) {
}
func (c *SkillRegistryConfig) UnmarshalJSON(data []byte) error {
type alias struct {
Name string `json:"name,omitempty"`
Enabled bool `json:"enabled"`
BaseURL string `json:"base_url"`
AuthToken SecureString `json:"auth_token,omitzero"`
Param map[string]any `json:"param,omitempty"`
}
var raw map[string]json.RawMessage
if err := json.Unmarshal(data, &raw); err != nil {
return err
}
var parsed alias
if err := json.Unmarshal(data, &parsed); err != nil {
return err
params := cloneRegistryParams(c.Param)
if params == nil {
params = map[string]any{}
}
params := map[string]any{}
for key, value := range parsed.Param {
params[key] = value
if value, ok := raw["name"]; ok {
if err := json.Unmarshal(value, &c.Name); err != nil {
return err
}
}
if value, ok := raw["enabled"]; ok {
if err := json.Unmarshal(value, &c.Enabled); err != nil {
return err
}
}
if value, ok := raw["base_url"]; ok {
if err := json.Unmarshal(value, &c.BaseURL); err != nil {
return err
}
}
if value, ok := raw["auth_token"]; ok {
if err := json.Unmarshal(value, &c.AuthToken); err != nil {
return err
}
}
if value, ok := raw["param"]; ok {
var nested map[string]any
if err := json.Unmarshal(value, &nested); err != nil {
return err
}
for key, nestedValue := range nested {
params[key] = nestedValue
}
}
for key, value := range raw {
switch key {
@ -397,10 +436,6 @@ func (c *SkillRegistryConfig) UnmarshalJSON(data []byte) error {
params[key] = decoded
}
}
c.Name = parsed.Name
c.Enabled = parsed.Enabled
c.BaseURL = parsed.BaseURL
c.AuthToken = parsed.AuthToken
c.Param = params
return nil
}
@ -433,7 +468,10 @@ func (c *SkillRegistryConfig) UnmarshalYAML(value *yaml.Node) error {
if err := value.Decode(&raw); err != nil {
return err
}
params := map[string]any{}
params := cloneRegistryParams(c.Param)
if params == nil {
params = map[string]any{}
}
if nested, ok := raw["param"].(map[string]any); ok {
for k, v := range nested {
params[k] = v
@ -523,33 +561,85 @@ func (v *SkillsRegistriesConfig) UnmarshalYAML(value *yaml.Node) error {
*v = list
return nil
}
for _, registry := range *v {
if registry == nil {
for _, name := range sortedRegistryNames(mm) {
sec := mm[name]
if sec == nil {
continue
}
sec := mm[registry.Name]
if sec != nil {
registry.AuthToken = sec.AuthToken
if registry.BaseURL == "" {
registry.BaseURL = sec.BaseURL
}
if !registry.Enabled {
registry.Enabled = sec.Enabled
}
if registry.Param == nil {
registry.Param = map[string]any{}
}
for key, value := range sec.Param {
if _, ok := registry.Param[key]; ok {
continue
}
registry.Param[key] = value
sec.Name = name
registry := findRegistryConfigByName(*v, name)
if registry == nil {
*v = append(*v, cloneRegistryConfig(sec))
continue
}
registry.AuthToken = sec.AuthToken
if registry.BaseURL == "" {
registry.BaseURL = sec.BaseURL
}
if !registry.Enabled {
registry.Enabled = sec.Enabled
}
if registry.Param == nil {
registry.Param = map[string]any{}
}
for key, value := range sec.Param {
if _, ok := registry.Param[key]; ok {
continue
}
registry.Param[key] = value
}
}
return nil
}
func cloneRegistryParams(src map[string]any) map[string]any {
if src == nil {
return nil
}
cloned := make(map[string]any, len(src))
for key, value := range src {
cloned[key] = value
}
return cloned
}
func cloneRegistryConfig(src *SkillRegistryConfig) *SkillRegistryConfig {
if src == nil {
return nil
}
cloned := *src
cloned.Param = cloneRegistryParams(src.Param)
return &cloned
}
func findRegistryConfigByName(registries SkillsRegistriesConfig, name string) *SkillRegistryConfig {
for _, registry := range registries {
if registry == nil || registry.Name != name {
continue
}
return registry
}
return nil
}
func sortedRegistryNames(mm map[string]*SkillRegistryConfig) []string {
keys := make([]string, 0, len(mm))
for name := range mm {
keys = append(keys, name)
}
sort.Strings(keys)
return keys
}
func sortedRegistryNamesFromJSON(mm map[string]json.RawMessage) []string {
keys := make([]string, 0, len(mm))
for name := range mm {
keys = append(keys, name)
}
sort.Strings(keys)
return keys
}
func (v SkillsRegistriesConfig) MarshalYAML() (any, error) {
type onlySecureRegistryData struct {
AuthToken SecureString `yaml:"auth_token,omitempty"`

View file

@ -263,6 +263,47 @@ func TestSkillsRegistriesConfigMarshalJSONPreservesObjectShape(t *testing.T) {
assert.Equal(t, "https://clawhub.ai", clawhub.BaseURL)
}
func TestSkillsRegistriesConfigUnmarshalJSONPreservesDefaultRegistries(t *testing.T) {
registries := DefaultConfig().Tools.Skills.Registries
err := json.Unmarshal([]byte(`{
"clawhub": {
"base_url": "https://clawhub.example.com"
}
}`), &registries)
assert.NoError(t, err)
clawhub, ok := registries.Get("clawhub")
assert.True(t, ok)
assert.True(t, clawhub.Enabled)
assert.Equal(t, "https://clawhub.example.com", clawhub.BaseURL)
github, ok := registries.Get("github")
assert.True(t, ok)
assert.True(t, github.Enabled)
assert.Equal(t, "https://github.com", github.BaseURL)
assert.Empty(t, github.Param)
}
func TestSkillsRegistriesConfigUnmarshalYAMLAppendsNewRegistryToExistingSlice(t *testing.T) {
registries := DefaultConfig().Tools.Skills.Registries
err := yaml.Unmarshal([]byte(`custom:
base_url: https://skills.example.com
auth_token: custom-token
`), &registries)
assert.NoError(t, err)
custom, ok := registries.Get("custom")
assert.True(t, ok)
assert.Equal(t, "https://skills.example.com", custom.BaseURL)
assert.Equal(t, "custom-token", custom.AuthToken.String())
github, ok := registries.Get("github")
assert.True(t, ok)
assert.Equal(t, "https://github.com", github.BaseURL)
}
func TestSkillsGithubConfigV0ToSkillsGithubConfigPreservesBaseURL(t *testing.T) {
legacy := skillsGithubConfigV0{
BaseURL: "https://ghe.example.com/git",

View file

@ -481,4 +481,50 @@ skills:
assert.Equal(t, "ghp-github-registry-token-from-file", githubRegistry.AuthToken.String())
assert.Equal(t, "http://127.0.0.1:7890", githubRegistry.Param["proxy"])
})
t.Run("Custom registry token supports security overlay", func(t *testing.T) {
tmpDir := t.TempDir()
customTokenFile := filepath.Join(tmpDir, "custom_registry_token.txt")
err := os.WriteFile(customTokenFile, []byte("custom-registry-token-from-file"), 0o600)
require.NoError(t, err)
configPath := filepath.Join(tmpDir, "config.json")
configContent := `{
"version": 1,
"tools": {
"skills": {
"registries": {
"custom": {
"enabled": true,
"base_url": "https://skills.example.com"
}
}
}
}
}`
err = os.WriteFile(configPath, []byte(configContent), 0o644)
require.NoError(t, err)
securityPath := filepath.Join(tmpDir, SecurityConfigFile)
securityContent := `skills:
registries:
custom:
auth_token: "file://custom_registry_token.txt"
`
err = os.WriteFile(securityPath, []byte(securityContent), 0o600)
require.NoError(t, err)
cfg, err := LoadConfig(configPath)
require.NoError(t, err)
customRegistry, ok := cfg.Tools.Skills.Registries.Get("custom")
require.True(t, ok)
assert.Equal(t, "https://skills.example.com", customRegistry.BaseURL)
assert.Equal(t, "custom-registry-token-from-file", customRegistry.AuthToken.String())
githubRegistry, ok := cfg.Tools.Skills.Registries.Get("github")
require.True(t, ok)
assert.Equal(t, "https://github.com", githubRegistry.BaseURL)
})
}