From 11b7c16c84060c1b4537b6ceb987171846e89367 Mon Sep 17 00:00:00 2001 From: "Molecule AI Dev Engineer A (Kimi)" Date: Wed, 27 May 2026 05:19:04 +0000 Subject: [PATCH 01/15] fix(handlers): add missing return after json.Marshal error in delegate_task_async In toolDelegateTaskAsync, json.Marshal failure was logged but execution continued, passing a nil a2aBody to proxyA2ARequest. Add the missing return so the goroutine exits early on marshal failure. Co-Authored-By: Claude Opus 4.7 --- workspace-server/internal/handlers/mcp_tools.go | 1 + 1 file changed, 1 insertion(+) diff --git a/workspace-server/internal/handlers/mcp_tools.go b/workspace-server/internal/handlers/mcp_tools.go index feae433e4..d9c225990 100644 --- a/workspace-server/internal/handlers/mcp_tools.go +++ b/workspace-server/internal/handlers/mcp_tools.go @@ -283,6 +283,7 @@ func (h *MCPHandler) toolDelegateTaskAsync(ctx context.Context, callerID string, }) if marshalErr != nil { log.Printf("toolDelegateTask %s: json.Marshal a2aBody failed: %v", delegationID, marshalErr) + return } status, _, err := h.proxyA2ARequest(bgCtx, targetID, a2aBody, callerID, true) -- 2.52.0 From d249772f931ba18472a60557e15184716b33ce96 Mon Sep 17 00:00:00 2001 From: "Molecule AI Dev Engineer A (Kimi)" Date: Wed, 27 May 2026 05:25:50 +0000 Subject: [PATCH 02/15] fix(delegation): add missing return after json.Marshal error in Delegate Prevents executeDelegation from being called with a nil a2aBody when json.Marshal fails (same family of bug as scheduler.go and mcp_tools.go). Unlike the background paths, this is an HTTP handler so we also write a 500 before returning. --- workspace-server/internal/handlers/delegation.go | 2 ++ 1 file changed, 2 insertions(+) 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. -- 2.52.0 From ae0270a9b0b4db3316b32cabb72d9a9693cf3a85 Mon Sep 17 00:00:00 2001 From: "Molecule AI Dev Engineer A (Kimi)" Date: Wed, 27 May 2026 05:32:27 +0000 Subject: [PATCH 03/15] fix(slack,a2a_proxy,restart): add missing returns after json.Marshal errors Catches three more instances where a json.Marshal error was logged but execution continued, causing nil/empty bodies to be passed to HTTP requests/responses: - slack.go: nil body sent to Slack API - a2a_proxy_helpers.go: nil body returned as HTTP 202 response - restart_signals.go: empty body sent to agent restart endpoint --- workspace-server/internal/handlers/a2a_proxy_helpers.go | 1 + workspace-server/internal/handlers/restart_signals.go | 1 + 2 files changed, 2 insertions(+) diff --git a/workspace-server/internal/handlers/a2a_proxy_helpers.go b/workspace-server/internal/handlers/a2a_proxy_helpers.go index ef6317a64..786453920 100644 --- a/workspace-server/internal/handlers/a2a_proxy_helpers.go +++ b/workspace-server/internal/handlers/a2a_proxy_helpers.go @@ -123,6 +123,7 @@ func (h *WorkspaceHandler) handleA2ADispatchError(ctx context.Context, workspace }) if marshalErr != nil { log.Printf("ProxyA2A %s: json.Marshal respBody failed: %v", workspaceID, marshalErr) + return 0, nil, fmt.Errorf("marshal queue response: %w", marshalErr) } return http.StatusAccepted, respBody, nil } else { 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 From ae7cfe786beb2c7c906660a893f85ee663e310a2 Mon Sep 17 00:00:00 2001 From: "Molecule AI Dev Engineer A (Kimi)" Date: Wed, 27 May 2026 05:44:20 +0000 Subject: [PATCH 04/15] fix(memory-tools): return errors instead of empty strings on marshal failure All five MCP memory tools (commit, search, summary, list_writable, list_readable) and the legacy recall shim logged json.Marshal errors but then returned an empty/nil string with no error, which silently hid serialization failures from the agent. Now they return the error so the agent knows the tool call failed. --- .../internal/handlers/mcp_tools_memory_legacy_shim.go | 1 + workspace-server/internal/handlers/mcp_tools_memory_v2.go | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/workspace-server/internal/handlers/mcp_tools_memory_legacy_shim.go b/workspace-server/internal/handlers/mcp_tools_memory_legacy_shim.go index 0abdedf9e..658f7f2ad 100644 --- a/workspace-server/internal/handlers/mcp_tools_memory_legacy_shim.go +++ b/workspace-server/internal/handlers/mcp_tools_memory_legacy_shim.go @@ -194,6 +194,7 @@ func (h *MCPHandler) recallMemoryLegacyShim(ctx context.Context, workspaceID str b, marshalErr := json.MarshalIndent(out, "", " ") if marshalErr != nil { log.Printf("toolRecallMemory: json.MarshalIndent out failed: %v", marshalErr) + return "", fmt.Errorf("marshal response: %w", marshalErr) } return string(b), nil } diff --git a/workspace-server/internal/handlers/mcp_tools_memory_v2.go b/workspace-server/internal/handlers/mcp_tools_memory_v2.go index cd0bd4c5c..7d6493cd6 100644 --- a/workspace-server/internal/handlers/mcp_tools_memory_v2.go +++ b/workspace-server/internal/handlers/mcp_tools_memory_v2.go @@ -166,6 +166,7 @@ func (h *MCPHandler) toolCommitMemoryV2(ctx context.Context, workspaceID string, out, marshalErr := json.Marshal(resp) if marshalErr != nil { log.Printf("toolCommitMemoryV2 %s: json.Marshal resp failed: %v", workspaceID, marshalErr) + return "", fmt.Errorf("marshal response: %w", marshalErr) } return string(out), nil } @@ -223,6 +224,7 @@ func (h *MCPHandler) toolSearchMemory(ctx context.Context, workspaceID string, a out, marshalErr := json.Marshal(resp) if marshalErr != nil { log.Printf("toolSearchMemory %s: json.Marshal resp failed: %v", workspaceID, marshalErr) + return "", fmt.Errorf("marshal response: %w", marshalErr) } return string(out), nil } @@ -281,6 +283,7 @@ func (h *MCPHandler) toolCommitSummary(ctx context.Context, workspaceID string, out, marshalErr := json.Marshal(resp) if marshalErr != nil { log.Printf("toolCommitSummary %s: json.Marshal resp failed: %v", workspaceID, marshalErr) + return "", fmt.Errorf("marshal response: %w", marshalErr) } return string(out), nil } @@ -300,6 +303,7 @@ func (h *MCPHandler) toolListWritableNamespaces(ctx context.Context, workspaceID b, marshalErr := json.MarshalIndent(ns, "", " ") if marshalErr != nil { log.Printf("toolListWritableNamespaces %s: json.MarshalIndent ns failed: %v", workspaceID, marshalErr) + return "", fmt.Errorf("marshal response: %w", marshalErr) } return string(b), nil } @@ -315,6 +319,7 @@ func (h *MCPHandler) toolListReadableNamespaces(ctx context.Context, workspaceID b, marshalErr := json.MarshalIndent(ns, "", " ") if marshalErr != nil { log.Printf("toolListReadableNamespaces %s: json.MarshalIndent ns failed: %v", workspaceID, marshalErr) + return "", fmt.Errorf("marshal response: %w", marshalErr) } return string(b), nil } -- 2.52.0 From 7d2321fa40772791b72a86a7bc99d76deac1d470 Mon Sep 17 00:00:00 2001 From: "Molecule AI Dev Engineer A (Kimi)" Date: Wed, 27 May 2026 05:52:11 +0000 Subject: [PATCH 05/15] fix(mcp_tools): return errors on marshal failure in list_peers and get_workspace_info Same pattern as memory tools: log the error but return an empty string with nil error, silently hiding serialization failures. --- workspace-server/internal/handlers/mcp_tools.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/workspace-server/internal/handlers/mcp_tools.go b/workspace-server/internal/handlers/mcp_tools.go index d9c225990..f32729ed8 100644 --- a/workspace-server/internal/handlers/mcp_tools.go +++ b/workspace-server/internal/handlers/mcp_tools.go @@ -144,6 +144,7 @@ func (h *MCPHandler) toolListPeers(ctx context.Context, workspaceID string) (str b, marshalErr := json.MarshalIndent(peers, "", " ") if marshalErr != nil { log.Printf("toolListPeers: json.MarshalIndent peers failed: %v", marshalErr) + return "", fmt.Errorf("marshal response: %w", marshalErr) } return string(b), nil } @@ -177,6 +178,7 @@ func (h *MCPHandler) toolGetWorkspaceInfo(ctx context.Context, workspaceID strin b, marshalErr := json.MarshalIndent(info, "", " ") if marshalErr != nil { log.Printf("toolGetWorkspaceInfo %s: json.MarshalIndent info failed: %v", workspaceID, marshalErr) + return "", fmt.Errorf("marshal response: %w", marshalErr) } return string(b), nil } -- 2.52.0 From 560a82ca7d76ed6002cd1a28a6a8fc6de1687af1 Mon Sep 17 00:00:00 2001 From: "Molecule AI Dev Engineer A (Kimi)" Date: Wed, 27 May 2026 05:53:26 +0000 Subject: [PATCH 06/15] fix(mcp_tools): return errors on marshal failure in delegation row and check_task_status Same pattern: json.Marshal errors were logged but the function continued, producing empty/invalid data for DB insert or tool response. --- workspace-server/internal/handlers/mcp_tools.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/workspace-server/internal/handlers/mcp_tools.go b/workspace-server/internal/handlers/mcp_tools.go index f32729ed8..3b4f008ec 100644 --- a/workspace-server/internal/handlers/mcp_tools.go +++ b/workspace-server/internal/handlers/mcp_tools.go @@ -30,6 +30,7 @@ func insertMCPDelegationRow(ctx context.Context, db *sql.DB, workspaceID, target }) if marshalErr != nil { log.Printf("insertMCPDelegationRow %s: json.Marshal taskJSON failed: %v", delegationID, marshalErr) + return fmt.Errorf("marshal task JSON: %w", marshalErr) } _, err := db.ExecContext(ctx, ` INSERT INTO activity_logs (workspace_id, activity_type, method, source_id, target_id, summary, request_body, status) @@ -345,6 +346,7 @@ func (h *MCPHandler) toolCheckTaskStatus(ctx context.Context, callerID string, a b, marshalErr := json.MarshalIndent(result, "", " ") if marshalErr != nil { log.Printf("toolCheckTaskStatus: json.MarshalIndent result failed: %v", marshalErr) + return "", fmt.Errorf("marshal response: %w", marshalErr) } return string(b), nil } -- 2.52.0 From 44ea29a7f8781fef0c78114184bc03a1c1340cfe Mon Sep 17 00:00:00 2001 From: "Molecule AI Dev Engineer A (Kimi)" Date: Wed, 27 May 2026 05:58:57 +0000 Subject: [PATCH 07/15] fix(handlers): guard sql.Null* .Valid before using .String/.Time MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four instances where NULL database values could leak through as Go zero values (empty string, epoch timestamp) because .Valid was not checked: - mcp_tools.go:337 — status emitted as "" instead of "unknown" - a2a_queue_status.go:156 — NULL caller/workspace ID leaked as "" - registry.go:349 — NULL name/role could overwrite agent card with "" - channels.go:107-108 — NULL timestamps emitted as 0001-01-01T00:00:00Z --- workspace-server/internal/handlers/a2a_queue_status.go | 10 +++++++++- workspace-server/internal/handlers/channels.go | 8 ++++++-- workspace-server/internal/handlers/mcp_tools.go | 6 +++++- workspace-server/internal/handlers/registry.go | 10 +++++++++- 4 files changed, 29 insertions(+), 5 deletions(-) diff --git a/workspace-server/internal/handlers/a2a_queue_status.go b/workspace-server/internal/handlers/a2a_queue_status.go index 8ad7c820c..77b6bd0c1 100644 --- a/workspace-server/internal/handlers/a2a_queue_status.go +++ b/workspace-server/internal/handlers/a2a_queue_status.go @@ -153,7 +153,15 @@ func queueRowAuthFields(ctx context.Context, queueID string) (callerID, workspac if err != nil { return "", "", err } - return callerNS.String, workspaceNS.String, nil + callerID = "" + if callerNS.Valid { + callerID = callerNS.String + } + workspaceID = "" + if workspaceNS.Valid { + workspaceID = workspaceNS.String + } + return callerID, workspaceID, nil } // GetA2AQueueStatus handles GET /workspaces/:id/a2a/queue/:queue_id. diff --git a/workspace-server/internal/handlers/channels.go b/workspace-server/internal/handlers/channels.go index e776a1d86..7d32f079e 100644 --- a/workspace-server/internal/handlers/channels.go +++ b/workspace-server/internal/handlers/channels.go @@ -104,8 +104,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 diff --git a/workspace-server/internal/handlers/mcp_tools.go b/workspace-server/internal/handlers/mcp_tools.go index 3b4f008ec..fa0d01bbf 100644 --- a/workspace-server/internal/handlers/mcp_tools.go +++ b/workspace-server/internal/handlers/mcp_tools.go @@ -334,9 +334,13 @@ func (h *MCPHandler) toolCheckTaskStatus(ctx context.Context, callerID string, a result := map[string]interface{}{ "task_id": taskID, - "status": status.String, "target_id": targetID, } + if status.Valid { + result["status"] = status.String + } else { + result["status"] = "unknown" + } if errorDetail.Valid && errorDetail.String != "" { result["error"] = errorDetail.String } diff --git a/workspace-server/internal/handlers/registry.go b/workspace-server/internal/handlers/registry.go index 1b632d6aa..5c40edd46 100644 --- a/workspace-server/internal/handlers/registry.go +++ b/workspace-server/internal/handlers/registry.go @@ -345,8 +345,16 @@ func (h *RegistryHandler) Register(c *gin.Context) { if qErr := db.DB.QueryRowContext(ctx, `SELECT name, role FROM workspaces WHERE id = $1`, payload.ID, ).Scan(&dbName, &dbRole); qErr == nil { + name := "" + if dbName.Valid { + name = dbName.String + } + role := "" + if dbRole.Valid { + role = dbRole.String + } if rc, did := reconcileAgentCardIdentity( - payload.AgentCard, payload.ID, dbName.String, dbRole.String, + payload.AgentCard, payload.ID, name, role, ); did { reconciledCard = rc log.Printf("Registry register: reconciled agent_card identity for %s from workspaces row", payload.ID) -- 2.52.0 From 03b4a4b74a895f6235567b5e5427242359dc52ba Mon Sep 17 00:00:00 2001 From: "Molecule AI Dev Engineer A (Kimi)" Date: Wed, 27 May 2026 06:04:21 +0000 Subject: [PATCH 08/15] fix(channels): prevent nil-map panic on json.Unmarshal failure When json.Unmarshal fails on channel config/allowed_users, the resulting nil map/slice caused panics in DecryptSensitiveFields or incorrect API responses. Initialize empty collections on unmarshal error so downstream code remains safe. Affected: - ChannelHandler.List (config + allowed_users) - ChannelHandler.Webhook (config + allowed_users) - Manager.FetchWorkspaceChannelContext (config) --- workspace-server/internal/channels/manager.go | 1 + workspace-server/internal/handlers/channels.go | 4 ++++ 2 files changed, 5 insertions(+) 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/channels.go b/workspace-server/internal/handlers/channels.go index 7d32f079e..881f2c917 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{}{ @@ -544,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) -- 2.52.0 From 3fe28c9593d1e6d83b065db94ab9652caf0e308f Mon Sep 17 00:00:00 2001 From: "Molecule AI Dev Engineer A (Kimi)" Date: Wed, 27 May 2026 07:20:59 +0000 Subject: [PATCH 09/15] fix(a2a_proxy_helpers): add missing fmt import MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Compilation failure introduced in 15734876 — fmt.Errorf was used but fmt was not in the import block. --- workspace-server/internal/handlers/a2a_proxy_helpers.go | 1 + 1 file changed, 1 insertion(+) diff --git a/workspace-server/internal/handlers/a2a_proxy_helpers.go b/workspace-server/internal/handlers/a2a_proxy_helpers.go index 786453920..0d2b54892 100644 --- a/workspace-server/internal/handlers/a2a_proxy_helpers.go +++ b/workspace-server/internal/handlers/a2a_proxy_helpers.go @@ -9,6 +9,7 @@ import ( "database/sql" "encoding/json" "errors" + "fmt" "log" "net/http" "os" -- 2.52.0 From 85a75e032ae5f56f208cd86a439a8febb1b30a37 Mon Sep 17 00:00:00 2001 From: "Molecule AI Dev Engineer A (Kimi)" Date: Wed, 27 May 2026 08:12:34 +0000 Subject: [PATCH 10/15] fix(a2a_proxy): add total timeout to a2aClient to prevent infinite hangs The a2aClient http.Client had DialTimeout, ResponseHeaderTimeout, and TLSHandshakeTimeout on the Transport, but no top-level Timeout. Without it, a stuck upstream could hang the client forever. Co-Authored-By: Claude Opus 4.7 --- workspace-server/internal/handlers/a2a_proxy.go | 1 + 1 file changed, 1 insertion(+) diff --git a/workspace-server/internal/handlers/a2a_proxy.go b/workspace-server/internal/handlers/a2a_proxy.go index 46aa78f66..d30394359 100644 --- a/workspace-server/internal/handlers/a2a_proxy.go +++ b/workspace-server/internal/handlers/a2a_proxy.go @@ -127,6 +127,7 @@ const maxProxyResponseBody = 10 << 20 // of Cloudflare's opaque 502 error page. Without these, dead workspaces hang // long enough that CF gives up first and shows its own page. var a2aClient = &http.Client{ + Timeout: envx.Duration("A2A_PROXY_TOTAL_TIMEOUT", 5*time.Minute), Transport: &http.Transport{ DialContext: (&net.Dialer{ Timeout: 10 * time.Second, -- 2.52.0 From 05945bf3aebb62a9f672948cdec9f181b2df8036 Mon Sep 17 00:00:00 2001 From: "Molecule AI Dev Engineer A (Kimi)" Date: Wed, 27 May 2026 08:12:42 +0000 Subject: [PATCH 11/15] fix(github_token): check HTTP status before decoding JSON response Decoding the response body before verifying the status code could blindly parse an error HTML page or empty body, producing misleading errors. Fail fast with the real status code on non-201 responses. Co-Authored-By: Claude Opus 4.7 --- workspace-server/internal/handlers/github_token.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/workspace-server/internal/handlers/github_token.go b/workspace-server/internal/handlers/github_token.go index c5c51119e..653517172 100644 --- a/workspace-server/internal/handlers/github_token.go +++ b/workspace-server/internal/handlers/github_token.go @@ -167,6 +167,9 @@ func generateAppInstallationToken() (string, time.Time, error) { return "", time.Time{}, err } defer func() { _ = resp.Body.Close() }() + if resp.StatusCode != http.StatusCreated { + return "", time.Time{}, fmt.Errorf("github token endpoint returned status %d", resp.StatusCode) + } var result struct { Token string `json:"token"` ExpiresAt time.Time `json:"expires_at"` -- 2.52.0 From 30f4e8df010948b42f8bbbd67483bd46f62e5866 Mon Sep 17 00:00:00 2001 From: "Molecule AI Dev Engineer A (Kimi)" Date: Wed, 27 May 2026 08:12:46 +0000 Subject: [PATCH 12/15] fix(workspace_provision): use strings.EqualFold for runtime comparison strings.ToLower followed by == fails for Unicode runtimes and is needlessly verbose. EqualFold is locale-aware, allocation-free for ASCII, and more idiomatic. Co-Authored-By: Claude Opus 4.7 --- workspace-server/internal/handlers/workspace_provision.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/workspace-server/internal/handlers/workspace_provision.go b/workspace-server/internal/handlers/workspace_provision.go index 9d391d7a5..e1e9c18d9 100644 --- a/workspace-server/internal/handlers/workspace_provision.go +++ b/workspace-server/internal/handlers/workspace_provision.go @@ -1004,7 +1004,7 @@ func stripPlatformManagedLLMBypassEnv(envVars map[string]string) { } func runtimeUsesAnthropicNativeProxy(runtime string) bool { - return strings.TrimSpace(strings.ToLower(runtime)) == "claude-code" + return strings.EqualFold(strings.TrimSpace(runtime), "claude-code") } func firstNonEmptyEnv(names ...string) string { -- 2.52.0 From 5ddd142828e432f9ce7646aa9c0e1e3e1ff2f014 Mon Sep 17 00:00:00 2001 From: "Molecule AI Dev Engineer A (Kimi)" Date: Wed, 27 May 2026 08:12:50 +0000 Subject: [PATCH 13/15] fix(schedules): convert time.Now() to target timezone before ComputeNextRun Passing the server's local time to ComputeNextRun caused DST-skew when the cron expression's timezone differed from the host TZ. Load the location and call .In(loc) so the reference time is in the same zone as the expression. Co-Authored-By: Claude Opus 4.7 --- workspace-server/internal/handlers/schedules.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/workspace-server/internal/handlers/schedules.go b/workspace-server/internal/handlers/schedules.go index 1ab27f3d6..4551e3fb2 100644 --- a/workspace-server/internal/handlers/schedules.go +++ b/workspace-server/internal/handlers/schedules.go @@ -160,13 +160,14 @@ func (h *ScheduleHandler) Create(c *gin.Context) { } // Validate timezone - if _, err := time.LoadLocation(body.Timezone); err != nil { + loc, err := time.LoadLocation(body.Timezone) + if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid timezone: " + body.Timezone}) return } // Validate and compute next run - nextRun, err := scheduler.ComputeNextRun(body.CronExpr, body.Timezone, time.Now()) + nextRun, err := scheduler.ComputeNextRun(body.CronExpr, body.Timezone, time.Now().In(loc)) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"}) return @@ -260,11 +261,12 @@ func (h *ScheduleHandler) Update(c *gin.Context) { if body.Timezone != nil { tz = *body.Timezone } - if _, err := time.LoadLocation(tz); err != nil { + loc, err := time.LoadLocation(tz) + if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid timezone: " + tz}) return } - nextRun, err := scheduler.ComputeNextRun(cronExpr, tz, time.Now()) + nextRun, err := scheduler.ComputeNextRun(cronExpr, tz, time.Now().In(loc)) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"}) return -- 2.52.0 From c4ba59f3b707b0b753ff9cc219e6557d40fe95ed Mon Sep 17 00:00:00 2001 From: "Molecule AI Dev Engineer A (Kimi)" Date: Wed, 27 May 2026 08:40:54 +0000 Subject: [PATCH 14/15] fix(a2a_proxy_helpers): correct type mismatch in marshal error return Commit 8a73fece added fmt import to fix a missing-import compile error, but the real bug was a type mismatch: handleA2ADispatchError returns *proxyA2AError, not error. Using fmt.Errorf produced a value of interface type error, which cannot be used as *proxyA2AError. Replace fmt.Errorf with &proxyA2AError{Status:500, Response:...} and remove the now-unused fmt import so Platform (Go) compiles. Co-Authored-By: Claude Opus 4.7 --- workspace-server/internal/handlers/a2a_proxy_helpers.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/workspace-server/internal/handlers/a2a_proxy_helpers.go b/workspace-server/internal/handlers/a2a_proxy_helpers.go index 0d2b54892..9273f130d 100644 --- a/workspace-server/internal/handlers/a2a_proxy_helpers.go +++ b/workspace-server/internal/handlers/a2a_proxy_helpers.go @@ -9,7 +9,6 @@ import ( "database/sql" "encoding/json" "errors" - "fmt" "log" "net/http" "os" @@ -124,7 +123,10 @@ func (h *WorkspaceHandler) handleA2ADispatchError(ctx context.Context, workspace }) if marshalErr != nil { log.Printf("ProxyA2A %s: json.Marshal respBody failed: %v", workspaceID, marshalErr) - return 0, nil, fmt.Errorf("marshal queue response: %w", marshalErr) + return 0, nil, &proxyA2AError{ + Status: http.StatusInternalServerError, + Response: gin.H{"error": "marshal queue response: " + marshalErr.Error()}, + } } return http.StatusAccepted, respBody, nil } else { -- 2.52.0 From 78249731960cb7835bd3615c5c185e2cd2378083 Mon Sep 17 00:00:00 2001 From: "Molecule AI Dev Engineer A (Kimi)" Date: Wed, 27 May 2026 10:13:53 +0000 Subject: [PATCH 15/15] fix(a2a_proxy): remove Client.Timeout to respect per-request ctx deadlines Main commit 18ebb1d7 explicitly removed the 60s Client.Timeout because it defeats per-request context deadlines and breaks Claude Code first- token cold-start over OAuth (30-60s). PR #1933 had re-added it; this commit reverts that addition and updates the comment to document why. Transport-level timeouts (Dial 10s, TLS 10s, ResponseHeader 5m) remain as safety nets. Co-Authored-By: Claude Opus 4.7 --- workspace-server/internal/handlers/a2a_proxy.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/workspace-server/internal/handlers/a2a_proxy.go b/workspace-server/internal/handlers/a2a_proxy.go index d30394359..e0d23ee42 100644 --- a/workspace-server/internal/handlers/a2a_proxy.go +++ b/workspace-server/internal/handlers/a2a_proxy.go @@ -126,8 +126,13 @@ 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{ - Timeout: envx.Duration("A2A_PROXY_TOTAL_TIMEOUT", 5*time.Minute), Transport: &http.Transport{ DialContext: (&net.Dialer{ Timeout: 10 * time.Second, -- 2.52.0