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
73 lines
2.3 KiB
Go
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
|
|
}
|