From 18ad66051cfaf13fe97bf17a51ed756b2c6373f1 Mon Sep 17 00:00:00 2001 From: "Molecule AI Dev Engineer A (Kimi)" Date: Thu, 28 May 2026 02:35:38 +0000 Subject: [PATCH] defensive: nil-map init, marshal-error returns, NullTime guards, a2a timeout comment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Residue from mol#1933 slim-down — only changes not already in main: - channels/manager.go, handlers/channels.go: initialise nil maps/slices after json.Unmarshal errors so downstream code doesn't panic on nil dereference. - handlers/channels.go: guard sql.NullTime .Time access with .Valid check (matches existing lastMsg.Valid pattern). - handlers/delegation.go, handlers/restart_signals.go: add missing return after json.Marshal error logs so we don't continue with zero/invalid payload. - handlers/a2a_proxy.go: document why a2aClient has no Client.Timeout (per-request ctx deadlines govern lifetime; fixed timeout would break legitimate slow cold-start flows). Co-Authored-By: Claude Opus 4.7 --- workspace-server/internal/channels/manager.go | 1 + workspace-server/internal/handlers/a2a_proxy.go | 6 ++++++ workspace-server/internal/handlers/channels.go | 12 ++++++++++-- workspace-server/internal/handlers/delegation.go | 2 ++ .../internal/handlers/restart_signals.go | 1 + 5 files changed, 20 insertions(+), 2 deletions(-) diff --git a/workspace-server/internal/channels/manager.go b/workspace-server/internal/channels/manager.go index b4c90c0fd..4fb4da5f3 100644 --- a/workspace-server/internal/channels/manager.go +++ b/workspace-server/internal/channels/manager.go @@ -532,6 +532,7 @@ func (m *Manager) FetchWorkspaceChannelContext(ctx context.Context, workspaceID var config map[string]interface{} if err := json.Unmarshal(configJSON, &config); err != nil { log.Printf("ChannelManager: unmarshal config: %v", err) + config = map[string]interface{}{} } if err := DecryptSensitiveFields(config); err != nil { return "" diff --git a/workspace-server/internal/handlers/a2a_proxy.go b/workspace-server/internal/handlers/a2a_proxy.go index 04f52af0a..0713bb304 100644 --- a/workspace-server/internal/handlers/a2a_proxy.go +++ b/workspace-server/internal/handlers/a2a_proxy.go @@ -126,6 +126,12 @@ const maxProxyResponseBody = 10 << 20 // gets `{"error":"workspace agent unreachable","restarting":true}` instead // of Cloudflare's opaque 502 error page. Without these, dead workspaces hang // long enough that CF gives up first and shows its own page. +// +// No Client.Timeout here — per-request context deadlines govern the full +// request lifetime (canvas = 5 min, agent-to-agent = 30 min). A fixed +// Client.Timeout would pre-empt legitimate slow cold-start flows (e.g. +// Claude Code first-token over OAuth can take 30-60s on boot). Transport- +// level timeouts (Dial, TLS, ResponseHeader) are sufficient safety nets. var a2aClient = &http.Client{ Transport: &http.Transport{ DialContext: (&net.Dialer{ diff --git a/workspace-server/internal/handlers/channels.go b/workspace-server/internal/handlers/channels.go index e776a1d86..176b8dfeb 100644 --- a/workspace-server/internal/handlers/channels.go +++ b/workspace-server/internal/handlers/channels.go @@ -73,6 +73,7 @@ func (h *ChannelHandler) List(c *gin.Context) { var config map[string]interface{} if err := json.Unmarshal(configJSON, &config); err != nil { log.Printf("Channels: unmarshal config for channel %s: %v", id, err) + config = map[string]interface{}{} } // #319: decrypt sensitive fields first so the mask operates on // plaintext (first-4 / last-4 of the real token, not the ciphertext @@ -94,6 +95,7 @@ func (h *ChannelHandler) List(c *gin.Context) { var allowed []string if err := json.Unmarshal(allowedJSON, &allowed); err != nil { log.Printf("Channels: unmarshal allowed_users for channel %s: %v", id, err) + allowed = []string{} } entry := map[string]interface{}{ @@ -104,8 +106,12 @@ func (h *ChannelHandler) List(c *gin.Context) { "enabled": enabled, "allowed_users": allowed, "message_count": msgCount, - "created_at": createdAt.Time, - "updated_at": updatedAt.Time, + } + if createdAt.Valid { + entry["created_at"] = createdAt.Time + } + if updatedAt.Valid { + entry["updated_at"] = updatedAt.Time } if lastMsg.Valid { entry["last_message_at"] = lastMsg.Time @@ -540,9 +546,11 @@ func (h *ChannelHandler) Webhook(c *gin.Context) { } if err := json.Unmarshal(configJSON, &row.Config); err != nil { log.Printf("Channels: unmarshal config for webhook row %s: %v", row.ID, err) + row.Config = map[string]interface{}{} } if err := json.Unmarshal(allowedJSON, &row.AllowedUsers); err != nil { log.Printf("Channels: unmarshal allowed_users for webhook row %s: %v", row.ID, err) + row.AllowedUsers = []string{} } if err := channels.DecryptSensitiveFields(row.Config); err != nil { log.Printf("Channels: decrypt webhook row %s: %v", row.ID, err) diff --git a/workspace-server/internal/handlers/delegation.go b/workspace-server/internal/handlers/delegation.go index c277d390c..78c8aa34e 100644 --- a/workspace-server/internal/handlers/delegation.go +++ b/workspace-server/internal/handlers/delegation.go @@ -186,6 +186,8 @@ func (h *DelegationHandler) Delegate(c *gin.Context) { }) if marshalErr != nil { log.Printf("Delegation %s: json.Marshal a2aBody failed: %v", delegationID, marshalErr) + c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to build A2A request"}) + return } // Fire-and-forget: send A2A in a background goroutine. diff --git a/workspace-server/internal/handlers/restart_signals.go b/workspace-server/internal/handlers/restart_signals.go index 4734717ea..7f501a1fc 100644 --- a/workspace-server/internal/handlers/restart_signals.go +++ b/workspace-server/internal/handlers/restart_signals.go @@ -83,6 +83,7 @@ func (h *WorkspaceHandler) gracefulPreRestart(ctx context.Context, workspaceID s body, marshalErr := json.Marshal(payload) if marshalErr != nil { log.Printf("A2AGracefulRestart %s: json.Marshal payload failed: %v", workspaceID, marshalErr) + return } req, reqErr := http.NewRequestWithContext(signalCtx, http.MethodPost, url, bytes.NewReader(body)) -- 2.52.0