Why: Allow consumers to download an immutable module revision without nested source checkouts or local path replacements. Evidence: SELF package tests and the prior canonical-path rejection are recorded in the Dev dependency-workspace task card; remote consumption is replayed against the committed candidate. Impact: Change the module identity and its Go imports while preserving API behavior. Existing historical submodule revisions remain buildable. Refs: Versioned dependencies U1/U3; feature/dependency-workspace
109 lines
2.9 KiB
Go
109 lines
2.9 KiB
Go
package settings
|
|
|
|
import (
|
|
core "git-ssh.u1680832.nyat.app/weichen/Agent-System-Core.git"
|
|
"strings"
|
|
)
|
|
|
|
// MaskedProjectMetadata returns a detached credential-safe view without changing
|
|
// stored values or dotted service/configuration key shapes. The builtin and
|
|
// plugin secret schema plus the existing secret-leaf fallback cover legacy data.
|
|
func (s *Service) MaskedProjectMetadata(values map[string]any) map[string]any {
|
|
if values == nil {
|
|
return map[string]any{}
|
|
}
|
|
schema := Schema()
|
|
if s != nil {
|
|
schema = append(schema, s.extraSchema...)
|
|
}
|
|
secrets := map[string]bool{}
|
|
for _, d := range schema {
|
|
if d.Type == core.SettingSecret || d.Secret {
|
|
secrets[d.Key] = true
|
|
}
|
|
}
|
|
return maskedProjectValue(values, "", secrets).(map[string]any)
|
|
}
|
|
|
|
func maskedProjectValue(value any, path string, secrets map[string]bool) any {
|
|
leaf := path
|
|
if i := strings.LastIndexByte(leaf, '.'); i >= 0 {
|
|
leaf = leaf[i+1:]
|
|
}
|
|
if secrets[path] || isSecretKey(leaf) {
|
|
if value == nil {
|
|
return nil
|
|
}
|
|
if text, ok := value.(string); ok && text == "" {
|
|
return text
|
|
}
|
|
return MaskPlaceholder
|
|
}
|
|
switch v := value.(type) {
|
|
case map[string]any:
|
|
if v == nil {
|
|
return v
|
|
}
|
|
out := make(map[string]any, len(v))
|
|
for key, item := range v {
|
|
child := key
|
|
if path != "" {
|
|
child = path + "." + key
|
|
}
|
|
out[key] = maskedProjectValue(item, child, secrets)
|
|
}
|
|
return out
|
|
case []any:
|
|
if v == nil {
|
|
return v
|
|
}
|
|
out := make([]any, len(v))
|
|
for i, item := range v {
|
|
out[i] = maskedProjectValue(item, path, secrets)
|
|
}
|
|
return out
|
|
default:
|
|
return value
|
|
}
|
|
}
|
|
|
|
// PrepareProjectMetadataWrite applies the same managed-path and mask-placeholder
|
|
// rules as project settings, while preserving the metadata store's key shapes.
|
|
// Project metadata lives in projects.settings, not the settings scope table.
|
|
func (s *Service) PrepareProjectMetadataWrite(values map[string]any) (map[string]any, error) {
|
|
incoming := dropProjectMaskPlaceholders(values)
|
|
if err := s.checkManagedWrite(normalizeKeys(incoming)); err != nil {
|
|
return nil, err
|
|
}
|
|
return incoming, nil
|
|
}
|
|
|
|
// dropProjectMaskPlaceholders prunes a parent only when a previously nonempty
|
|
// object became empty through mask removal. Explicit empty objects still express
|
|
// an overwrite and must reach the managed-path guard unchanged.
|
|
func dropProjectMaskPlaceholders(values map[string]any) map[string]any {
|
|
out := make(map[string]any, len(values))
|
|
for key, value := range values {
|
|
if child, ok := value.(map[string]any); ok {
|
|
prepared := dropProjectMaskPlaceholders(child)
|
|
if len(child) > 0 && len(prepared) == 0 {
|
|
continue
|
|
}
|
|
out[key] = prepared
|
|
continue
|
|
}
|
|
if text, ok := value.(string); ok && text == MaskPlaceholder {
|
|
continue
|
|
}
|
|
out[key] = value
|
|
}
|
|
return out
|
|
}
|
|
|
|
func (s *Service) checkManagedWrite(incoming map[string]any) error {
|
|
if hits := core.TouchesManagedSettings(incoming, s.ManagedSettingPaths()); len(hits) > 0 {
|
|
return &ErrManagedPath{Paths: hits}
|
|
}
|
|
return nil
|
|
}
|