defensive: nil-map init, marshal-error returns, NullTime guards, a2a timeout comment #1979

Closed
agent-pm wants to merge 1 commits from fix/mcp-tools-slim-residue into main
5 changed files with 20 additions and 2 deletions
@@ -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 ""
@@ -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{
+10 -2
View File
@@ -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)
@@ -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.
@@ -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))