From 713bb16012df153cd5ab278fba8f08d7558c9aa3 Mon Sep 17 00:00:00 2001 From: Rodribm10 Date: Fri, 1 May 2026 15:42:57 -0300 Subject: [PATCH] =?UTF-8?q?feat(captain):=20MCP=20controller=20aceita=20Be?= =?UTF-8?q?arer=20token=20al=C3=A9m=20de=20HMAC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hermes Agent (cliente MCP) usa `--auth header` que envia `Authorization: Bearer ` — padrão MCP. Antes o Captain MCP server só aceitava HMAC-SHA256 (X-Hub-Signature-256), incompatível com o que Hermes manda nativamente. Agora aceita qualquer um dos 2 modos: - Bearer token (recomendado, padrão MCP) — Hermes envia automaticamente - HMAC-SHA256 do body — pra clientes que preferem assinar payload Ambos validados com ActiveSupport::SecurityUtils.secure_compare contra o mesmo CAPTAIN_MCP_SECRET. Sem secret = validação desabilitada (PoC/dev). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../webhooks/captain/mcp_controller.rb | 31 ++++++++++++++----- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/app/controllers/webhooks/captain/mcp_controller.rb b/app/controllers/webhooks/captain/mcp_controller.rb index 3e6b18afc..c9d563354 100644 --- a/app/controllers/webhooks/captain/mcp_controller.rb +++ b/app/controllers/webhooks/captain/mcp_controller.rb @@ -8,9 +8,13 @@ # Conexão pelo Hermes: # hermes mcp add captain-tools --url http://CAPTAIN_HOST/webhooks/captain/mcp # -# Auth: HMAC-SHA256 do body via header `X-Hub-Signature-256`, secret -# compartilhado via env var `CAPTAIN_MCP_SECRET` (igual ao padrão de -# `hermes_callback`). Quando vazio, validação é desabilitada (PoC/dev). +# Auth: aceita 2 modos (qualquer um basta): +# - Bearer token (padrão MCP, recomendado): `Authorization: Bearer ` +# É o que `hermes mcp add --auth header` usa nativamente. +# - HMAC-SHA256 do body: `X-Hub-Signature-256: sha256=` +# Para clientes que preferem assinar o body inteiro. +# Secret compartilhado via env var `CAPTAIN_MCP_SECRET`. Quando vazio, +# validação é desabilitada (PoC/dev). # # Multi-tenant: o cliente MCP pode mandar contexto (conversation_id, # inbox_id, account_id) num campo de extensão chamado `_captain_context` @@ -50,13 +54,26 @@ class Webhooks::Captain::McpController < ApplicationController secret = ENV.fetch('CAPTAIN_MCP_SECRET', nil) return true if secret.blank? + return true if bearer_token_matches?(secret) + return true if hmac_signature_matches?(secret) + + head :unauthorized + end + + def bearer_token_matches?(secret) + auth_header = request.headers['Authorization'].to_s + return false unless auth_header.start_with?('Bearer ') + + token = auth_header.delete_prefix('Bearer ').strip + ActiveSupport::SecurityUtils.secure_compare(token, secret) + end + + def hmac_signature_matches?(secret) signature = request.headers['X-Hub-Signature-256'].to_s - return head :unauthorized if signature.blank? + return false if signature.blank? expected = "sha256=#{OpenSSL::HMAC.hexdigest('SHA256', secret, request.raw_post)}" - return head :unauthorized unless ActiveSupport::SecurityUtils.secure_compare(signature, expected) - - true + ActiveSupport::SecurityUtils.secure_compare(signature, expected) end # Cliente MCP pode mandar contexto multi-tenant em params._captain_context.