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 }