package proxy import ( "context" "encoding/json" "fmt" "io" "net/http" "time" "chorus/pkg/seqthink/ageio" "chorus/pkg/seqthink/mcpclient" "github.com/rs/zerolog/log" ) // handleToolCallEncrypted proxies encrypted tool calls to MCP server (Beat 2) func (s *Server) handleToolCallEncrypted(w http.ResponseWriter, r *http.Request) { s.config.Metrics.IncrementRequests() startTime := time.Now() // Check Content-Type header contentType := r.Header.Get("Content-Type") if contentType != "application/age" { log.Error(). Str("content_type", contentType). Msg("Invalid Content-Type, expected application/age") s.config.Metrics.IncrementErrors() http.Error(w, "Content-Type must be application/age", http.StatusUnsupportedMediaType) return } // Limit request body size r.Body = http.MaxBytesReader(w, r.Body, int64(s.config.MaxBodyMB)*1024*1024) // Read encrypted request body encryptedBody, err := io.ReadAll(r.Body) if err != nil { log.Error().Err(err).Msg("Failed to read encrypted request body") s.config.Metrics.IncrementErrors() http.Error(w, "Failed to read request", http.StatusBadRequest) return } // Create decryptor decryptor, err := ageio.NewDecryptor(s.config.AgeIdentPath) if err != nil { log.Error().Err(err).Msg("Failed to create decryptor") s.config.Metrics.IncrementErrors() http.Error(w, "Decryption initialization failed", http.StatusInternalServerError) return } // Decrypt request plaintext, err := decryptor.Decrypt(encryptedBody) if err != nil { log.Error().Err(err).Msg("Failed to decrypt request") s.config.Metrics.IncrementDecryptFails() http.Error(w, "Decryption failed", http.StatusBadRequest) return } log.Debug(). Int("encrypted_size", len(encryptedBody)). Int("plaintext_size", len(plaintext)). Msg("Request decrypted successfully") // Parse tool request var toolReq mcpclient.ToolRequest if err := json.Unmarshal(plaintext, &toolReq); err != nil { log.Error().Err(err).Msg("Failed to parse decrypted tool request") s.config.Metrics.IncrementErrors() http.Error(w, "Invalid request format", http.StatusBadRequest) return } log.Info(). Str("tool", toolReq.Tool). Msg("Proxying encrypted tool call to MCP server") // Call MCP server (plaintext internally) ctx, cancel := context.WithTimeout(r.Context(), 120*time.Second) defer cancel() toolResp, err := s.config.MCPClient.CallTool(ctx, &toolReq) if err != nil { log.Error().Err(err).Msg("MCP tool call failed") s.config.Metrics.IncrementErrors() http.Error(w, fmt.Sprintf("Tool call failed: %v", err), http.StatusInternalServerError) return } // Serialize response responseJSON, err := json.Marshal(toolResp) if err != nil { log.Error().Err(err).Msg("Failed to marshal response") s.config.Metrics.IncrementErrors() http.Error(w, "Response serialization failed", http.StatusInternalServerError) return } // Create encryptor encryptor, err := ageio.NewEncryptor(s.config.AgeRecipsPath) if err != nil { log.Error().Err(err).Msg("Failed to create encryptor") s.config.Metrics.IncrementErrors() http.Error(w, "Encryption initialization failed", http.StatusInternalServerError) return } // Encrypt response encryptedResponse, err := encryptor.Encrypt(responseJSON) if err != nil { log.Error().Err(err).Msg("Failed to encrypt response") s.config.Metrics.IncrementEncryptFails() http.Error(w, "Encryption failed", http.StatusInternalServerError) return } log.Debug(). Int("plaintext_size", len(responseJSON)). Int("encrypted_size", len(encryptedResponse)). Msg("Response encrypted successfully") // Return encrypted response w.Header().Set("Content-Type", "application/age") w.WriteHeader(http.StatusOK) if _, err := w.Write(encryptedResponse); err != nil { log.Error().Err(err).Msg("Failed to write encrypted response") s.config.Metrics.IncrementErrors() return } duration := time.Since(startTime) s.config.Metrics.ObserveRequestDuration(duration.Seconds()) log.Info(). Str("tool", toolReq.Tool). Dur("duration", duration). Bool("encrypted", true). Msg("Tool call completed") }