Skip to content

Commit 4e33c92

Browse files
author
AGI Developer
committed
fix: address lint and data race in annotation overrides
- httpapi: remove redundant nil check before len (S1009) and simplify restartRequired assignment (QF1007) - config: remove unused helpers annotationsEqual/boolPtrEqual - server: remove unused test helpers servedHints/boolVal - server: fix UpdateServer data race by copy-on-write for runtime Config snapshot (mirrors configWithAppendedServer pattern)
1 parent 74eda95 commit 4e33c92

4 files changed

Lines changed: 13 additions & 79 deletions

File tree

‎internal/config/annotation_overrides.go‎

Lines changed: 0 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -61,43 +61,6 @@ func EffectiveAnnotationsForTool(overrides map[string]*ToolAnnotations, toolName
6161
return base
6262
}
6363

64-
// annotationsEqual reports value equality for two ToolAnnotations, comparing
65-
// Title and dereferenced *bool hints (nil == nil). Pointer identity is ignored.
66-
func annotationsEqual(a, b *ToolAnnotations) bool {
67-
if a == nil && b == nil {
68-
return true
69-
}
70-
if a == nil || b == nil {
71-
return false
72-
}
73-
if a.Title != b.Title {
74-
return false
75-
}
76-
if !boolPtrEqual(a.ReadOnlyHint, b.ReadOnlyHint) {
77-
return false
78-
}
79-
if !boolPtrEqual(a.DestructiveHint, b.DestructiveHint) {
80-
return false
81-
}
82-
if !boolPtrEqual(a.IdempotentHint, b.IdempotentHint) {
83-
return false
84-
}
85-
if !boolPtrEqual(a.OpenWorldHint, b.OpenWorldHint) {
86-
return false
87-
}
88-
return true
89-
}
90-
91-
func boolPtrEqual(a, b *bool) bool {
92-
if a == nil && b == nil {
93-
return true
94-
}
95-
if a == nil || b == nil {
96-
return false
97-
}
98-
return *a == *b
99-
}
100-
10164
// CloneAnnotationOverrides deep-copies an annotation_overrides map (values and
10265
// their *bool hints) so hot-reload snapshots never alias the caller's map.
10366
// Nil in, nil out; nil values are preserved as nil.

‎internal/httpapi/server.go‎

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2655,7 +2655,7 @@ func (s *Server) handleAddServer(w http.ResponseWriter, r *http.Request) {
26552655
// Validate annotation overrides via ValidateDetailed (write gate) before
26562656
// persisting. Reuses the PATCH pattern: temporary config with the new
26572657
// server appended, filtered to annotation_overrides errors.
2658-
if serverConfig.AnnotationOverrides != nil && len(serverConfig.AnnotationOverrides) > 0 {
2658+
if len(serverConfig.AnnotationOverrides) > 0 {
26592659
cfg, err := s.controller.GetConfig()
26602660
if err != nil {
26612661
s.writeError(w, r, http.StatusInternalServerError, fmt.Sprintf("Failed to load current config for annotation validation: %v", err))
@@ -2705,7 +2705,7 @@ func (s *Server) handleAddServer(w http.ResponseWriter, r *http.Request) {
27052705
}
27062706

27072707
logger.Infow("Server added successfully", "server", req.Name, "quarantined", quarantined)
2708-
if s.auditSink != nil && serverConfig.AnnotationOverrides != nil && len(serverConfig.AnnotationOverrides) > 0 {
2708+
if s.auditSink != nil && len(serverConfig.AnnotationOverrides) > 0 {
27092709
afterMap := httpAnnotationOverridesToAuditMap(serverConfig.AnnotationOverrides)
27102710
if len(afterMap) > 0 {
27112711
if line, lerr := audit.NewConfigChange(audit.ConfigChangeInput{
@@ -3249,10 +3249,7 @@ func (s *Server) handlePatchServer(w http.ResponseWriter, r *http.Request) {
32493249
}
32503250
}
32513251
}
3252-
restartRequired := true
3253-
if onlyAnnotationOverrideHot {
3254-
restartRequired = false
3255-
}
3252+
restartRequired := !onlyAnnotationOverrideHot
32563253
s.writeSuccess(w, map[string]interface{}{
32573254
"message": fmt.Sprintf("Server '%s' updated successfully", serverName),
32583255
"restart_required": restartRequired,

‎internal/server/annotation_override_immediate_refresh_test.go‎

Lines changed: 1 addition & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -34,37 +34,8 @@ import (
3434
"github.com/smart-mcp-proxy/mcpproxy-go/internal/runtime"
3535
)
3636

37-
// servedHints returns the (destructiveHint, readOnlyHint) the production GET
38-
// path would serve for the named tool (Runtime.GetServerTools → StateView
39-
// snapshot). The snapshot carries *config.ToolAnnotations, not JSON maps.
40-
func servedHints(t *testing.T, rt *runtime.Runtime, server, tool string) (destructive, readOnly *bool) {
41-
t.Helper()
42-
tools, err := rt.GetServerTools(server)
43-
require.NoError(t, err)
44-
for _, tm := range tools {
45-
if tm["name"] == tool {
46-
ann, ok := tm["annotations"].(*config.ToolAnnotations)
47-
require.True(t, ok, "served annotations must be *config.ToolAnnotations, got %T", tm["annotations"])
48-
if ann == nil {
49-
return nil, nil
50-
}
51-
return ann.DestructiveHint, ann.ReadOnlyHint
52-
}
53-
}
54-
t.Fatalf("tool %q not served for server %q", tool, server)
55-
return nil, nil
56-
}
57-
58-
func boolVal(b *bool) interface{} {
59-
if b == nil {
60-
return nil
61-
}
62-
return *b
63-
}
64-
6537
// servedAnnotations is the map form the existing test body expects (keys
66-
// "destructiveHint"/"readOnlyHint" etc). Kept for the test assertions that
67-
// compare via map lookup; built from the same StateView snapshot as servedHints.
38+
// "destructiveHint"/"readOnlyHint" etc) for assertions that compare via map lookup.
6839
func servedAnnotations(t *testing.T, rt *runtime.Runtime, server, tool string) map[string]interface{} {
6940
t.Helper()
7041
tools, err := rt.GetServerTools(server)

‎internal/server/server.go‎

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2282,16 +2282,19 @@ func (s *Server) PatchServer(ctx context.Context, serverName string, resolve fun
22822282
}
22832283
}
22842284

2285-
// Update runtime config
2286-
currentConfig := s.runtime.Config()
2287-
if currentConfig != nil {
2288-
for i, sc := range currentConfig.Servers {
2285+
// Update runtime config — copy-on-write: Config() is the published
2286+
// snapshot that DiscoverAndIndexTools and others range over without a
2287+
// lock, so mutating its Servers slice in place is a DATA RACE.
2288+
if currentConfig := s.runtime.Config(); currentConfig != nil {
2289+
updated := *currentConfig
2290+
updated.Servers = append([]*config.ServerConfig(nil), currentConfig.Servers...)
2291+
for i, sc := range updated.Servers {
22892292
if sc.Name == serverName {
2290-
currentConfig.Servers[i] = existing
2293+
updated.Servers[i] = existing
22912294
break
22922295
}
22932296
}
2294-
s.runtime.UpdateConfig(currentConfig, "")
2297+
s.runtime.UpdateConfig(&updated, "")
22952298
}
22962299

22972300
// Push the refreshed config to the live manager client so hot-reloadable

0 commit comments

Comments
 (0)