diff --git a/workspace-server/internal/handlers/a2a_proxy.go b/workspace-server/internal/handlers/a2a_proxy.go index 4d57b6c9..4d4f23f2 100644 --- a/workspace-server/internal/handlers/a2a_proxy.go +++ b/workspace-server/internal/handlers/a2a_proxy.go @@ -87,7 +87,9 @@ const maxProxyResponseBody = 10 << 20 // a2aClient is a shared HTTP client for proxying A2A requests to workspace agents. // No client-level timeout — timeouts are enforced per-request via context deadlines: // canvas = 5 min (Rule 3), agent-to-agent = 30 min (DoS cap). -var a2aClient = &http.Client{} +var a2aClient = &http.Client{ + Timeout: 60 * time.Second, // Safety net for when context deadlines are missing +} type proxyA2AError struct { Status int diff --git a/workspace-server/internal/handlers/secrets.go b/workspace-server/internal/handlers/secrets.go index 256d102c..dd7abe05 100644 --- a/workspace-server/internal/handlers/secrets.go +++ b/workspace-server/internal/handlers/secrets.go @@ -276,7 +276,10 @@ func (h *SecretsHandler) Delete(c *gin.Context) { return } - rows, _ := result.RowsAffected() + rows, err := result.RowsAffected() + if err != nil { + log.Printf("DeleteWorkspace: RowsAffected error: %v", err) + } if rows == 0 { c.JSON(http.StatusNotFound, gin.H{"error": "secret not found"}) return @@ -418,7 +421,10 @@ func (h *SecretsHandler) DeleteGlobal(c *gin.Context) { return } - rows, _ := result.RowsAffected() + rows, err := result.RowsAffected() + if err != nil { + log.Printf("DeleteGlobal: RowsAffected error: %v", err) + } if rows == 0 { c.JSON(http.StatusNotFound, gin.H{"error": "secret not found"}) return diff --git a/workspace-server/internal/handlers/terminal.go b/workspace-server/internal/handlers/terminal.go index 45f6dc60..14afef20 100644 --- a/workspace-server/internal/handlers/terminal.go +++ b/workspace-server/internal/handlers/terminal.go @@ -97,7 +97,6 @@ func (h *TerminalHandler) HandleConnect(c *gin.Context) { log.Printf("Terminal WebSocket upgrade error: %v", err) return } - defer conn.Close() // No hard session deadline — terminal stays open as long as there is activity. // The idle timeout (terminalSessionTimeout) resets on each keystroke in the @@ -108,6 +107,7 @@ func (h *TerminalHandler) HandleConnect(c *gin.Context) { // ContainerExecCreate succeeds even if the binary doesn't exist — the error // only surfaces at attach/start time, so we must retry at the attach level. var resp types.HijackedResponse + var execErr error for _, shell := range []string{"/bin/bash", "/bin/sh"} { execCfg := container.ExecOptions{ Cmd: []string{shell}, @@ -118,20 +118,21 @@ func (h *TerminalHandler) HandleConnect(c *gin.Context) { } execID, createErr := h.docker.ContainerExecCreate(ctx, containerName, execCfg) if createErr != nil { - err = createErr + execErr = createErr continue } - resp, err = h.docker.ContainerExecAttach(ctx, execID.ID, container.ExecAttachOptions{Tty: true}) - if err == nil { + resp, execErr = h.docker.ContainerExecAttach(ctx, execID.ID, container.ExecAttachOptions{Tty: true}) + if execErr == nil { + defer resp.Close() break } } - if err != nil { - log.Printf("Terminal exec error: %v", err) + if execErr != nil { + log.Printf("Terminal exec error: %v", execErr) conn.WriteMessage(websocket.TextMessage, []byte("Error: failed to create shell session\r\n")) + conn.Close() return } - defer resp.Close() // Bridge: container stdout → WebSocket done := make(chan struct{}) diff --git a/workspace-server/internal/handlers/webhooks.go b/workspace-server/internal/handlers/webhooks.go index 7abfceb0..78173d36 100644 --- a/workspace-server/internal/handlers/webhooks.go +++ b/workspace-server/internal/handlers/webhooks.go @@ -33,6 +33,15 @@ func NewWebhookHandlerWithWorkspace(workspaces *WorkspaceHandler) *WebhookHandle } } +// shortSHA returns the first n characters of a commit SHA, or the +// full value if it's shorter than n. Safe for empty strings. +func shortSHA(sha string) string { + if len(sha) < 7 { + return sha + } + return sha[:7] +} + // GitHub handles POST /webhooks/github/:id // It verifies X-Hub-Signature-256, maps supported events to A2A message/send, // then forwards through the same proxy flow used by /workspaces/:id/a2a. @@ -266,7 +275,7 @@ func buildGitHubA2APayload(eventType, deliveryID string, rawBody []byte) (string payload.WorkflowRun.RunNumber, payload.WorkflowRun.Conclusion, payload.WorkflowRun.HeadBranch, - payload.WorkflowRun.HeadSHA[:min(7, len(payload.WorkflowRun.HeadSHA))], + shortSHA(payload.WorkflowRun.HeadSHA), payload.Sender.Login, payload.WorkflowRun.Event, payload.Repository.FullName,