diff --git a/workspace-server/cmd/server/cp_config.go b/workspace-server/cmd/server/cp_config.go index d1021c22f..d6d2e8d5e 100644 --- a/workspace-server/cmd/server/cp_config.go +++ b/workspace-server/cmd/server/cp_config.go @@ -60,7 +60,8 @@ func refreshEnvFromCP() error { req.Header.Set("Authorization", "Bearer "+adminToken) req.Header.Set("X-Molecule-Org-Id", orgID) - resp, err := http.DefaultClient.Do(req) + client := &http.Client{Timeout: 10 * time.Second} + resp, err := client.Do(req) if err != nil { return fmt.Errorf("do request: %w", err) } diff --git a/workspace-server/internal/bundle/importer.go b/workspace-server/internal/bundle/importer.go index d8ce175be..49d027f99 100644 --- a/workspace-server/internal/bundle/importer.go +++ b/workspace-server/internal/bundle/importer.go @@ -89,6 +89,11 @@ func Import( // PluginsPath set by caller if available } go func() { + defer func() { + if r := recover(); r != nil { + log.Printf("bundle/importer: PANIC during provision start for %s: %v", wsID, r) + } + }() provCtx, cancel := context.WithTimeout(context.Background(), provisioner.ProvisionTimeout) defer cancel() url, err := prov.Start(provCtx, cfg) diff --git a/workspace-server/internal/handlers/a2a_proxy.go b/workspace-server/internal/handlers/a2a_proxy.go index 17321a352..ce6e546ff 100644 --- a/workspace-server/internal/handlers/a2a_proxy.go +++ b/workspace-server/internal/handlers/a2a_proxy.go @@ -1002,7 +1002,12 @@ func applyIdleTimeout(parent context.Context, b *events.Broadcaster, workspaceID // completed when t.Cleanup fires. Does NOT read db.DB; idle-timer // management only. go func() { - defer unsub() + defer func() { + if r := recover(); r != nil { + log.Printf("a2a_proxy: PANIC in SSE idle watcher for %s: %v", workspaceID, r) + } + unsub() + }() timer := time.NewTimer(idle) defer timer.Stop() for { diff --git a/workspace-server/internal/handlers/discovery.go b/workspace-server/internal/handlers/discovery.go index ce679b29d..2eb79581e 100644 --- a/workspace-server/internal/handlers/discovery.go +++ b/workspace-server/internal/handlers/discovery.go @@ -249,7 +249,7 @@ func (h *DiscoveryHandler) Peers(c *gin.Context) { // parent_id-bound branch enumerates siblings, and that is already scoped to // one parent (one tenant). if parentID.Valid { - siblings, _ := queryPeerMaps(` + siblings, _ := queryPeerMaps(ctx, ` SELECT w.id, w.name, COALESCE(w.role, ''), w.tier, w.status, COALESCE(w.agent_card, 'null'::jsonb), COALESCE(w.url, ''), w.parent_id, w.active_tasks @@ -268,7 +268,7 @@ func (h *DiscoveryHandler) Peers(c *gin.Context) { // self-delegation 400 in a tight loop (#383). The `w.id != $2` // clause makes self-delegation-via-peer-list impossible regardless // of DB state. - children, _ := queryPeerMaps(` + children, _ := queryPeerMaps(ctx, ` SELECT w.id, w.name, COALESCE(w.role, ''), w.tier, w.status, COALESCE(w.agent_card, 'null'::jsonb), COALESCE(w.url, ''), w.parent_id, w.active_tasks @@ -281,7 +281,7 @@ func (h *DiscoveryHandler) Peers(c *gin.Context) { // propagate that corruption back to the agent as a "peer who is also // you" entry. if parentID.Valid { - parent, _ := queryPeerMaps(` + parent, _ := queryPeerMaps(ctx, ` SELECT w.id, w.name, COALESCE(w.role, ''), w.tier, w.status, COALESCE(w.agent_card, 'null'::jsonb), COALESCE(w.url, ''), w.parent_id, w.active_tasks @@ -350,8 +350,8 @@ func filterPeersByQuery(peers []map[string]interface{}, q string) []map[string]i } // queryPeerMaps returns clean JSON-serializable maps instead of Workspace structs. -func queryPeerMaps(query string, args ...interface{}) ([]map[string]interface{}, error) { - rows, err := db.DB.Query(query, args...) +func queryPeerMaps(ctx context.Context, query string, args ...interface{}) ([]map[string]interface{}, error) { + rows, err := db.DB.QueryContext(ctx, query, args...) if err != nil { log.Printf("queryPeerMaps error: %v", err) return nil, err diff --git a/workspace-server/internal/handlers/terminal.go b/workspace-server/internal/handlers/terminal.go index 99a32fc99..40148366a 100644 --- a/workspace-server/internal/handlers/terminal.go +++ b/workspace-server/internal/handlers/terminal.go @@ -217,7 +217,12 @@ func (h *TerminalHandler) handleLocalConnect(c *gin.Context, workspaceID string) // synchronously. No db.DB access on this path. done := make(chan struct{}) go func() { - defer close(done) + defer func() { + if r := recover(); r != nil { + log.Printf("Terminal: PANIC in stdout bridge: %v", r) + } + close(done) + }() buf := make([]byte, 4096) for { n, err := resp.Reader.Read(buf) @@ -440,7 +445,12 @@ func (h *TerminalHandler) handleRemoteConnect(c *gin.Context, workspaceID, insta // goAsync-exempt (RFC internal#524 Layer 2.2): WebSocket-lifetime // I/O bridge; handler blocks on `done` below. No db.DB access. go func() { - defer close(done) + defer func() { + if r := recover(); r != nil { + log.Printf("Terminal: PANIC in PTY bridge: %v", r) + } + close(done) + }() buf := make([]byte, 4096) for { n, err := ptmx.Read(buf) @@ -463,6 +473,11 @@ func (h *TerminalHandler) handleRemoteConnect(c *gin.Context, workspaceID, insta // WebSocket → PTY (stdin) // goAsync-exempt (RFC internal#524 Layer 2.2): see above. go func() { + defer func() { + if r := recover(); r != nil { + log.Printf("Terminal: PANIC in stdin loop: %v", r) + } + }() for { _, msg, rErr := conn.ReadMessage() if rErr != nil { diff --git a/workspace-server/internal/handlers/workspace.go b/workspace-server/internal/handlers/workspace.go index 30c453aed..062c7035f 100644 --- a/workspace-server/internal/handlers/workspace.go +++ b/workspace-server/internal/handlers/workspace.go @@ -530,6 +530,7 @@ func (h *WorkspaceHandler) Create(c *gin.Context) { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create workspace"}) return } + defer func() { _ = tx.Rollback() }() maxConcurrent := payload.MaxConcurrentTasks if maxConcurrent <= 0 { diff --git a/workspace-server/internal/middleware/mcp_ratelimit.go b/workspace-server/internal/middleware/mcp_ratelimit.go index c8f76b578..3133e2358 100644 --- a/workspace-server/internal/middleware/mcp_ratelimit.go +++ b/workspace-server/internal/middleware/mcp_ratelimit.go @@ -4,6 +4,7 @@ import ( "context" "crypto/sha256" "fmt" + "log" "net/http" "strconv" "strings" @@ -41,6 +42,11 @@ func NewMCPRateLimiter(rate int, interval time.Duration, ctx context.Context) *M interval: interval, } go func() { + defer func() { + if r := recover(); r != nil { + log.Printf("mcp_ratelimit: PANIC in bucket cleanup: %v", r) + } + }() ticker := time.NewTicker(5 * time.Minute) defer ticker.Stop() for { diff --git a/workspace-server/internal/middleware/ratelimit.go b/workspace-server/internal/middleware/ratelimit.go index e01324d39..c3c09dbfc 100644 --- a/workspace-server/internal/middleware/ratelimit.go +++ b/workspace-server/internal/middleware/ratelimit.go @@ -3,6 +3,7 @@ package middleware import ( "context" + "log" "net/http" "strconv" "strings" @@ -35,6 +36,11 @@ func NewRateLimiter(rate int, interval time.Duration, ctx context.Context) *Rate interval: interval, } go func() { + defer func() { + if r := recover(); r != nil { + log.Printf("ratelimit: PANIC in bucket cleanup: %v", r) + } + }() ticker := time.NewTicker(5 * time.Minute) defer ticker.Stop() for { diff --git a/workspace-server/internal/middleware/session_auth.go b/workspace-server/internal/middleware/session_auth.go index 3f6d058d6..62c7a970b 100644 --- a/workspace-server/internal/middleware/session_auth.go +++ b/workspace-server/internal/middleware/session_auth.go @@ -116,6 +116,11 @@ func sessionCachePut(key string, ok bool) { func init() { go func() { + defer func() { + if r := recover(); r != nil { + log.Printf("session_auth: PANIC in cache sweeper: %v", r) + } + }() // Jitter startup so restarts don't align sweeps. time.Sleep(time.Duration(rand.Int64N(int64(sessionCacheSweepEvery)))) t := time.NewTicker(sessionCacheSweepEvery)