diff --git a/CHANGELOG b/CHANGELOG index 79d0e2b81..c5c95645a 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,8 @@ v5.2.0 (July 2026) - UI: add dark mode support to the login and setup wizard pages + - Bugs fixes: + - Echo: stop broadcasting AI provider error responses to the browser + - Echo: restrict Anthropic provider requests to the fixed messages endpoint - Upgraded gems: - concurrent-ruby, crass, faraday, msgpack, net-imap, nokogiri, puma diff --git a/engines/dradis-echo/app/jobs/dradis/plugins/echo/interaction_job.rb b/engines/dradis-echo/app/jobs/dradis/plugins/echo/interaction_job.rb index 0a358f50e..24257e22c 100644 --- a/engines/dradis-echo/app/jobs/dradis/plugins/echo/interaction_job.rb +++ b/engines/dradis-echo/app/jobs/dradis/plugins/echo/interaction_job.rb @@ -23,10 +23,10 @@ def perform(agent_id:, prompt:, interaction_id:, response_id:) Turbo::StreamsChannel.broadcast_append_to [interaction_id, 'prompts'], target: 'messages', html: '

Done.

' rescue => e - msg = '
' - msg << ERB::Util.html_escape(e.message) - msg << '
' - Turbo::StreamsChannel.broadcast_update_to [interaction_id, 'prompts'], target: response_id, html: msg + Rails.logger.error("[Echo] interaction #{interaction_id} failed: #{e.message}") + + html = '
Something went wrong.
' + Turbo::StreamsChannel.broadcast_update_to [interaction_id, 'prompts'], target: response_id, html: html end end end diff --git a/engines/dradis-echo/app/models/dradis/plugins/echo/provider/anthropic.rb b/engines/dradis-echo/app/models/dradis/plugins/echo/provider/anthropic.rb index def48e118..18573abe6 100644 --- a/engines/dradis-echo/app/models/dradis/plugins/echo/provider/anthropic.rb +++ b/engines/dradis-echo/app/models/dradis/plugins/echo/provider/anthropic.rb @@ -3,7 +3,7 @@ class Provider::Anthropic < Provider include Provider::HttpStreaming API_VERSION = '2023-06-01'.freeze - DEFAULT_ADDRESS = 'https://api.anthropic.com/v1/messages'.freeze + DEFAULT_ADDRESS = 'https://api.anthropic.com/v1'.freeze DEFAULT_MAX_TOKENS = 4096 DEFAULT_MODEL = 'claude-sonnet-4-6'.freeze @@ -21,12 +21,12 @@ def build_body(prompt:, model:) def build_headers { 'anthropic-version' => API_VERSION, - 'x-api-key' => api_key + 'x-api-key' => api_key } end def build_uri(_model) - URI(address) + URI("#{address}/messages") end # Anthropic sends several SSE event types; only content_block_delta carries text: diff --git a/engines/dradis-echo/spec/factories/providers.rb b/engines/dradis-echo/spec/factories/providers.rb index ffa57ab9e..abc77e069 100644 --- a/engines/dradis-echo/spec/factories/providers.rb +++ b/engines/dradis-echo/spec/factories/providers.rb @@ -5,7 +5,7 @@ model { 'qwen2.5:14b' } factory :anthropic_provider, class: 'Dradis::Plugins::Echo::Provider::Anthropic' do - address { 'https://api.anthropic.com/v1/messages' } + address { 'https://api.anthropic.com/v1' } api_key { 'sk-ant-test' } model { 'claude-sonnet-4-6' } end diff --git a/engines/dradis-echo/spec/jobs/dradis/plugins/echo/interaction_job_spec.rb b/engines/dradis-echo/spec/jobs/dradis/plugins/echo/interaction_job_spec.rb index 9827e9a81..5c2989f11 100644 --- a/engines/dradis-echo/spec/jobs/dradis/plugins/echo/interaction_job_spec.rb +++ b/engines/dradis-echo/spec/jobs/dradis/plugins/echo/interaction_job_spec.rb @@ -4,9 +4,9 @@ describe Dradis::Plugins::Echo::InteractionJob do let(:interaction_id) { 'project-1' } - let(:response_id) { 'response-1' } - let(:prompt) { 'Summarise this issue.' } - let(:agent) { create(:system_agent) } + let(:response_id) { 'response-1' } + let(:prompt) { 'Summarise this issue.' } + let(:agent) { create(:system_agent) } def perform described_class.perform_now( @@ -26,25 +26,35 @@ def perform describe 'when agent is not enabled' do before { agent.update!(enabled: false) } - it 'broadcasts a user-friendly error' do + it 'broadcasts a generic error without leaking details' do perform expect(Turbo::StreamsChannel).to have_received(:broadcast_update_to) do |_, **kwargs| - expect(kwargs[:html]).to include('is not enabled') + expect(kwargs[:html]).to include('Something went wrong') + expect(kwargs[:html]).not_to include('is not enabled') end end end - describe 'error message sanitisation' do - it 'HTML-escapes the error message before broadcasting' do + describe 'error message handling' do + it 'does not reflect the underlying error message to the browser' do allow_any_instance_of(Dradis::Plugins::Echo::Provider::Ollama) - .to receive(:generate).and_raise('') + .to receive(:generate).and_raise(' upstream-secret') perform expect(Turbo::StreamsChannel).to have_received(:broadcast_update_to) do |_, **kwargs| - expect(kwargs[:html]).to include('<script>') - expect(kwargs[:html]).not_to include('