fix: multiple platform handler bug fixes

- secrets.go: Log RowsAffected errors instead of silently discarding them
- a2a_proxy.go: Add 60s safety timeout to a2aClient HTTP client
- terminal.go: Fix defer ordering - always close WebSocket conn on error,
  only defer resp.Close() after successful exec attach
- webhooks.go: Add shortSHA() helper to safely handle empty HeadSHA

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Molecule AI Platform Engineer 2026-04-20 05:01:01 +00:00
parent 14c36e1bbd
commit 87778c5c1b
4 changed files with 29 additions and 11 deletions

View File

@ -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

View File

@ -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

View File

@ -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{})

View File

@ -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,