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('