diff --git a/workspace-server/internal/handlers/a2a_proxy.go b/workspace-server/internal/handlers/a2a_proxy.go index d1707070..044d7c49 100644 --- a/workspace-server/internal/handlers/a2a_proxy.go +++ b/workspace-server/internal/handlers/a2a_proxy.go @@ -89,11 +89,13 @@ func isSystemCaller(callerID string) bool { 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{ - Timeout: 60 * time.Second, // Safety net for when context deadlines are missing -} +// No client-level timeout — timeouts are enforced per-request via context +// deadlines: canvas = 5 min (Rule 3), agent-to-agent = 30 min (DoS cap). Do NOT +// set a Client.Timeout here: it is enforced independently of ctx deadlines and +// would pre-empt legitimate slow cold-start flows (e.g. Claude Code first-token +// over OAuth can take 30-60s on boot). Callers that want a safety net should +// build a context.WithTimeout themselves. +var a2aClient = &http.Client{} type proxyA2AError struct { Status int diff --git a/workspace-server/internal/handlers/templates.go b/workspace-server/internal/handlers/templates.go index 833fb73c..737f5b06 100644 --- a/workspace-server/internal/handlers/templates.go +++ b/workspace-server/internal/handlers/templates.go @@ -298,9 +298,13 @@ func (h *TemplatesHandler) ReadFile(c *gin.Context) { return } - // Try container first + // Try container first. `cat` wants a single path argument — passing + // rootPath and filePath as two args would make `cat` try to read the + // rootPath directory (error) and then resolve filePath relative to + // the container's cwd, which isn't guaranteed to equal rootPath. if containerName := h.findContainer(ctx, workspaceID); containerName != "" { - content, err := h.execInContainer(ctx, containerName, []string{"cat", rootPath, filePath}) + fullPath := strings.TrimRight(rootPath, "/") + "/" + filePath + content, err := h.execInContainer(ctx, containerName, []string{"cat", fullPath}) if err == nil { c.JSON(http.StatusOK, gin.H{ "path": filePath,