diff --git a/docsgpt/api/answer/services/stream_processor.py b/docsgpt/api/answer/services/stream_processor.py index e810ea2a..2ce88a70 100644 --- a/docsgpt/api/answer/services/stream_processor.py +++ b/docsgpt/api/answer/services/stream_processor.py @@ -396,7 +396,13 @@ class StreamProcessor: share it with the rest of the turn. It is always generated here, never taken from the request body: request quotas count distinct request ids, so a client-chosen id would let every call count as one. + + A request with neither a token nor an agent API key has nobody to run + for: nothing is set up (no pre-fetch runs the agent's tools), None is + returned and the route answers 401. """ + if not self.decoded_token and not self.data.get("api_key"): + return None if not getattr(self, "request_id", None): self.request_id = str(uuid.uuid4()) self.initialize() diff --git a/tests/api/answer/test_stream_processor_exposure.py b/tests/api/answer/test_stream_processor_exposure.py index 582dac2f..6f2a4896 100644 --- a/tests/api/answer/test_stream_processor_exposure.py +++ b/tests/api/answer/test_stream_processor_exposure.py @@ -22,6 +22,7 @@ def _processor() -> StreamProcessor: sp.agent_config = {} sp.retriever_config = {"retriever_name": "classic", "chunks": 2, "doc_token_limit": 50000} sp.data = {} + sp.decoded_token = {"sub": "u"} return sp diff --git a/tests/api/answer/test_unauthenticated_runs.py b/tests/api/answer/test_unauthenticated_runs.py new file mode 100644 index 00000000..3fd2fe45 --- /dev/null +++ b/tests/api/answer/test_unauthenticated_runs.py @@ -0,0 +1,59 @@ +"""A request with no token and no API key never sets up an agent run. + +The routes answer it with 401, but they used to build the agent first, so a +public agent's prompt tools were pre-fetched (run) for nobody. +""" + +from __future__ import annotations + +from unittest.mock import MagicMock, patch + +import pytest + + +@pytest.mark.unit +class TestNoTokenNoRun: + def test_build_agent_sets_nothing_up_without_a_caller(self): + from docsgpt.api.answer.services.stream_processor import StreamProcessor + + processor = StreamProcessor({"question": "q", "agent_id": "a-1"}, None) + with patch.object(processor, "initialize") as initialize, \ + patch.object(processor, "pre_fetch_tools") as pre_fetch_tools, \ + patch.object(processor, "create_agent") as create_agent: + assert processor.build_agent("q") is None + initialize.assert_not_called() + pre_fetch_tools.assert_not_called() + create_agent.assert_not_called() + assert processor.decoded_token is None + + def test_api_key_request_still_runs(self): + from docsgpt.api.answer.services.stream_processor import StreamProcessor + + processor = StreamProcessor({"question": "q", "api_key": "k"}, None) + agent = MagicMock() + + def _initialize(): + processor.decoded_token = {"sub": "owner"} + + with patch.object(processor, "initialize", side_effect=_initialize), \ + patch.object(processor, "_exposure_partition", return_value=([], [])), \ + patch.object(processor, "pre_fetch_docs", return_value=(None, None)), \ + patch.object(processor, "pre_fetch_tools", return_value=None), \ + patch.object(processor, "create_agent", return_value=agent): + assert processor.build_agent("q") is agent + + def test_stream_route_answers_401_without_pre_fetching(self): + from flask import Flask + + from docsgpt.api.answer.routes.stream import StreamResource + + app = Flask(__name__) + with app.test_request_context("/stream", method="POST", json={"question": "q", "agent_id": "a-1"}), \ + patch("docsgpt.api.answer.services.stream_processor.StreamProcessor.pre_fetch_tools") as pre, \ + patch("docsgpt.api.answer.services.stream_processor.StreamProcessor.initialize"): + from flask import request + + request.decoded_token = None + response = StreamResource().post() + assert response.status_code == 401 + pre.assert_not_called()