Files
Agent-System-Backend/internal/settings/shared.go
T
weichen f965c5877f Enforce settings ownership and versioned shared configuration
Why: Personal project rows and credentials could override system policy and split shared business configuration by actor.

Evidence: SELF receipts /tmp/project-settings-backend-full-20261014-973d52c1.json and /tmp/rdm-settings-backend-final-green-20261014-03.json preserve full commands, object snapshots and output. PostgreSQL and current-head CI remain NOT_RUN.

Impact: Add scope-aware APIs, a fail-closed optional project configuration authorizer, CAS updates and explicit atomic migration. Preserve legacy rows and private preferences; protected secrets and runtime model routes obey system ownership.

Refs: P1/P2/P8, docs/records/2026-10-14-rdm-settings-completion-contract.md
2026-10-14 02:28:41 +00:00

73 lines
2.3 KiB
Go

package settings
import (
"errors"
"reflect"
"time"
core "agent-system-core"
)
var ErrSettingsConflict = errors.New("项目设置已被其他成员修改,请重新读取后保存")
// SharedProjectScope reads only the project-owned namespace. Old per-owner
// business copies are never silently promoted; migration inventory tracks them.
func (s *Service) SharedProjectScope(projectID string) (map[string]any, int64, error) {
row, err := s.st.GetSettingsScope(OwnerSystemRow, ScopeSharedProjectPrefix+projectID)
if err != nil {
return nil, 0, err
}
values, _ := row["values"].(map[string]any)
return s.ProjectValues(values, ProjectSettingScope), sharedRevision(row), nil
}
func sharedRevision(row map[string]any) int64 {
switch value := row["revision"].(type) {
case float64:
return int64(value)
case int64:
return value
case int:
return int64(value)
}
return 0
}
func (s *Service) MergeSharedProject(projectID string, values map[string]any, expectedRevision int64, unset ...[]string) (int64, error) {
if err := s.ValidateWrite(values, ProjectSettingScope); err != nil {
return 0, err
}
paths := optionalUnset(unset)
if err := s.validateUnset(values, paths, ProjectSettingScope); err != nil {
return 0, err
}
return s.mergeSharedProject(projectID, prepareWrite(values), &expectedRevision, paths)
}
func (s *Service) mergeSharedProject(projectID string, values map[string]any, expectedRevision *int64, unset ...[]string) (int64, error) {
if projectID == "" {
return 0, &ErrInvalidSetting{Path: "project", Reason: "需要项目标识"}
}
var revision int64
err := s.st.UpdateSettingsScope(OwnerSystemRow, ScopeSharedProjectPrefix+projectID, func(row map[string]any) (map[string]any, error) {
revision = sharedRevision(row)
if expectedRevision != nil && revision != *expectedRevision {
return nil, ErrSettingsConflict
}
current, _ := row["values"].(map[string]any)
if current == nil {
current = map[string]any{}
}
next := core.MergeSettings(s.ProjectValues(current, ProjectSettingScope), values)
for _, path := range optionalUnset(unset) {
deletePath(next, path)
}
if reflect.DeepEqual(current, next) {
return row, nil
}
revision++
return map[string]any{"revision": revision, "values": next, "updatedAt": time.Now().UTC().Format(time.RFC3339Nano)}, nil
})
return revision, err
}