fix skills registry config merge compatibility
This commit is contained in:
parent
528bb66750
commit
70c200e4d3
3 changed files with 220 additions and 43 deletions
|
|
@ -334,20 +334,41 @@ func (v *SkillsRegistriesConfig) UnmarshalJSON(data []byte) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
legacy := map[string]*SkillRegistryConfig{}
|
legacy := map[string]json.RawMessage{}
|
||||||
if err := json.Unmarshal(data, &legacy); err != nil {
|
if err := json.Unmarshal(data, &legacy); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
list = make([]*SkillRegistryConfig, 0, len(legacy))
|
if len(*v) == 0 {
|
||||||
for name, registry := range legacy {
|
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], ®istry); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
registry.Name = name
|
||||||
|
list = append(list, ®istry)
|
||||||
|
}
|
||||||
|
*v = list
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, name := range sortedRegistryNamesFromJSON(legacy) {
|
||||||
|
registry := cloneRegistryConfig(findRegistryConfigByName(*v, name))
|
||||||
if registry == nil {
|
if registry == nil {
|
||||||
continue
|
registry = &SkillRegistryConfig{Name: name}
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(legacy[name], registry); err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
registry.Name = name
|
registry.Name = name
|
||||||
list = append(list, registry)
|
v.Set(name, *registry)
|
||||||
}
|
}
|
||||||
*v = list
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -366,24 +387,42 @@ func (v SkillsRegistriesConfig) MarshalJSON() ([]byte, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *SkillRegistryConfig) UnmarshalJSON(data []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
|
var raw map[string]json.RawMessage
|
||||||
if err := json.Unmarshal(data, &raw); err != nil {
|
if err := json.Unmarshal(data, &raw); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
var parsed alias
|
params := cloneRegistryParams(c.Param)
|
||||||
if err := json.Unmarshal(data, &parsed); err != nil {
|
if params == nil {
|
||||||
return err
|
params = map[string]any{}
|
||||||
}
|
}
|
||||||
params := map[string]any{}
|
if value, ok := raw["name"]; ok {
|
||||||
for key, value := range parsed.Param {
|
if err := json.Unmarshal(value, &c.Name); err != nil {
|
||||||
params[key] = value
|
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 {
|
for key, value := range raw {
|
||||||
switch key {
|
switch key {
|
||||||
|
|
@ -397,10 +436,6 @@ func (c *SkillRegistryConfig) UnmarshalJSON(data []byte) error {
|
||||||
params[key] = decoded
|
params[key] = decoded
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
c.Name = parsed.Name
|
|
||||||
c.Enabled = parsed.Enabled
|
|
||||||
c.BaseURL = parsed.BaseURL
|
|
||||||
c.AuthToken = parsed.AuthToken
|
|
||||||
c.Param = params
|
c.Param = params
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
@ -433,7 +468,10 @@ func (c *SkillRegistryConfig) UnmarshalYAML(value *yaml.Node) error {
|
||||||
if err := value.Decode(&raw); err != nil {
|
if err := value.Decode(&raw); err != nil {
|
||||||
return err
|
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 {
|
if nested, ok := raw["param"].(map[string]any); ok {
|
||||||
for k, v := range nested {
|
for k, v := range nested {
|
||||||
params[k] = v
|
params[k] = v
|
||||||
|
|
@ -523,33 +561,85 @@ func (v *SkillsRegistriesConfig) UnmarshalYAML(value *yaml.Node) error {
|
||||||
*v = list
|
*v = list
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
for _, registry := range *v {
|
for _, name := range sortedRegistryNames(mm) {
|
||||||
if registry == nil {
|
sec := mm[name]
|
||||||
|
if sec == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
sec := mm[registry.Name]
|
sec.Name = name
|
||||||
if sec != nil {
|
registry := findRegistryConfigByName(*v, name)
|
||||||
registry.AuthToken = sec.AuthToken
|
if registry == nil {
|
||||||
if registry.BaseURL == "" {
|
*v = append(*v, cloneRegistryConfig(sec))
|
||||||
registry.BaseURL = sec.BaseURL
|
continue
|
||||||
}
|
}
|
||||||
if !registry.Enabled {
|
registry.AuthToken = sec.AuthToken
|
||||||
registry.Enabled = sec.Enabled
|
if registry.BaseURL == "" {
|
||||||
}
|
registry.BaseURL = sec.BaseURL
|
||||||
if registry.Param == nil {
|
}
|
||||||
registry.Param = map[string]any{}
|
if !registry.Enabled {
|
||||||
}
|
registry.Enabled = sec.Enabled
|
||||||
for key, value := range sec.Param {
|
}
|
||||||
if _, ok := registry.Param[key]; ok {
|
if registry.Param == nil {
|
||||||
continue
|
registry.Param = map[string]any{}
|
||||||
}
|
}
|
||||||
registry.Param[key] = value
|
for key, value := range sec.Param {
|
||||||
|
if _, ok := registry.Param[key]; ok {
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
registry.Param[key] = value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
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) {
|
func (v SkillsRegistriesConfig) MarshalYAML() (any, error) {
|
||||||
type onlySecureRegistryData struct {
|
type onlySecureRegistryData struct {
|
||||||
AuthToken SecureString `yaml:"auth_token,omitempty"`
|
AuthToken SecureString `yaml:"auth_token,omitempty"`
|
||||||
|
|
|
||||||
|
|
@ -263,6 +263,47 @@ func TestSkillsRegistriesConfigMarshalJSONPreservesObjectShape(t *testing.T) {
|
||||||
assert.Equal(t, "https://clawhub.ai", clawhub.BaseURL)
|
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"
|
||||||
|
}
|
||||||
|
}`), ®istries)
|
||||||
|
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
|
||||||
|
`), ®istries)
|
||||||
|
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) {
|
func TestSkillsGithubConfigV0ToSkillsGithubConfigPreservesBaseURL(t *testing.T) {
|
||||||
legacy := skillsGithubConfigV0{
|
legacy := skillsGithubConfigV0{
|
||||||
BaseURL: "https://ghe.example.com/git",
|
BaseURL: "https://ghe.example.com/git",
|
||||||
|
|
|
||||||
|
|
@ -481,4 +481,50 @@ skills:
|
||||||
assert.Equal(t, "ghp-github-registry-token-from-file", githubRegistry.AuthToken.String())
|
assert.Equal(t, "ghp-github-registry-token-from-file", githubRegistry.AuthToken.String())
|
||||||
assert.Equal(t, "http://127.0.0.1:7890", githubRegistry.Param["proxy"])
|
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)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue