cleanup: убрать мёртвые env-переменные, консолидировать чтение в agent/_config.py

Удалены из кода:
- JWT_SECRET — мёртвая (decode_token использует AUTH_SECRET_KEY)
- SESSION_SECRET_KEY — заменён на прямой AUTH_SECRET_KEY
- POSTGRES_URL — deprecated fallback, удалён из database.py и reencrypt.py

Консолидировано чтение env-переменных agent-модуля:
- Создан agent/_config.py — единый модуль для FASTAPI_URL,
  SERVICE_JWT, GRADIO_*, STORAGE_ROOT, AGENT_* (9 констант)
- Все agent/*.py импортируют из _config вместо разрозненных os.getenv

Удалены or-дефолты (безопасность):
- agent/langgraph_setup.py — удалён hardcoded DB URL postgres:postgres
- agent/langgraph_setup.py — удалены fallback API URL и model name
- scripts/reencrypt.py — удалён hardcoded DB URL postgres:postgres
- plugins/llm_analysis/service.py — удалены or-дефолты URL/app name

.env.example — минимализация:
- backend/.env.example: только 4 обязательные переменные
- root/.env.example: обязательные + docker + SSO/админ

Обновлены тесты (139 passed)
This commit is contained in:
2026-07-04 14:58:43 +03:00
parent a99c1d6d01
commit ec9bc76a37
16 changed files with 163 additions and 179 deletions

View File

@@ -50,7 +50,7 @@ async def test_handler_empty_message_returns_immediately():
mock_request.client.host = "127.0.0.1"
# Patch create_agent to avoid OpenAI init
with patch("src.agent.app.create_agent") as mock_create:
with patch("src.agent.langgraph_setup.create_agent") as mock_create:
# Empty message
message = {"text": "", "files": None}
results = []
@@ -231,7 +231,7 @@ async def test_handler_resume_deny():
message = {"text": "deny", "files": None}
with patch("src.agent.app.create_agent") as mock_create:
with patch("src.agent._confirmation.create_agent") as mock_create:
mock_graph = MagicMock()
mock_create.return_value = mock_graph

View File

@@ -403,9 +403,8 @@ def test_dual_auth_headers_no_user_jwt():
assert headers.get("Authorization") == "Bearer svc-token"
def test_dual_auth_headers_no_jwts(monkeypatch):
"""_dual_auth_headers returns empty dict when no JWTs."""
monkeypatch.delenv("SERVICE_JWT", raising=False)
def test_dual_auth_headers_no_jwts():
"""_dual_auth_headers falls back to _SERVICE_JWT when context vars are empty."""
from src.agent.context import set_service_jwt, set_user_jwt
from src.agent.tools import _dual_auth_headers
@@ -413,6 +412,9 @@ def test_dual_auth_headers_no_jwts(monkeypatch):
set_user_jwt("")
headers = _dual_auth_headers()
assert headers == {}
# Context vars are empty, _SERVICE_JWT is module-level constant from _config
# (set at import time based on os.environ)
assert "Authorization" in headers
assert headers["Authorization"].startswith("Bearer ")
# #endregion TestAgentChat.Tools.DualAuthHeaders
# #endregion TestAgentChat.Tools

View File

@@ -65,24 +65,12 @@ class TestCreateAgent:
ls._llm_config = None
@pytest.mark.anyio
async def test_creates_agent_with_env_fallback(self):
async def test_raises_error_when_no_llm_configured(self):
import src.agent.langgraph_setup as ls
ls._llm_config = None # Reset
with patch("src.agent.langgraph_setup.ChatOpenAI") as mock_llm, \
patch("src.agent.langgraph_setup.create_react_agent") as mock_create, \
patch("src.agent.langgraph_setup._fetch_llm_config", new=AsyncMock(return_value=None)), \
patch("src.agent.langgraph_setup.os.getenv") as mock_getenv:
mock_getenv.side_effect = lambda key, default=None: {
"LLM_API_KEY": "sk-env-key",
"LLM_BASE_URL": "https://env.api.com",
"LLM_MODEL": "gpt-4",
}.get(key, default)
mock_create.return_value = MagicMock()
result = await ls.create_agent([])
assert result is mock_create.return_value
call_kwargs = mock_llm.call_args[1]
assert call_kwargs["api_key"] == "sk-env-key"
assert call_kwargs["model"] == "gpt-4"
with patch("src.agent.langgraph_setup._fetch_llm_config", new=AsyncMock(return_value=None)):
with pytest.raises(RuntimeError, match="No LLM provider configured in backend"):
await ls.create_agent([])
ls._llm_config = None
@pytest.mark.anyio
@@ -101,18 +89,23 @@ class TestCreateAgent:
assert result is mock_create.return_value
call_kwargs = mock_llm.call_args[1]
assert call_kwargs["api_key"] == "sk-key-only"
assert call_kwargs["base_url"] == "https://api.openai.com/v1"
assert call_kwargs["model"] == "gpt-4o-mini"
assert call_kwargs["base_url"] is None
assert call_kwargs["model"] is None
ls._llm_config = None
@pytest.mark.anyio
async def test_uses_inmemory_saver(self):
import src.agent.langgraph_setup as ls
ls._llm_config = None
ls.configure_from_api({
"configured": True,
"api_key": "sk-test",
"base_url": "",
"default_model": "gpt-4o-mini",
})
with patch("src.agent.langgraph_setup.ChatOpenAI") as mock_llm, \
patch("src.agent.langgraph_setup.create_react_agent") as mock_create, \
patch("src.agent.langgraph_setup._fetch_llm_config", new=AsyncMock(return_value=None)), \
patch("src.agent.langgraph_setup.os.getenv", return_value=None):
patch("src.agent.langgraph_setup._fetch_llm_config", new=AsyncMock(return_value=ls._llm_config)):
mock_create.return_value = MagicMock()
await ls.create_agent([])
call_kwargs = mock_create.call_args[1]
@@ -124,10 +117,13 @@ class TestCreateAgent:
async def test_uses_empty_interrupt_list_by_default(self):
import src.agent.langgraph_setup as ls
ls._llm_config = None
ls.configure_from_api({
"configured": True,
"api_key": "sk-test",
})
with patch("src.agent.langgraph_setup.ChatOpenAI"), \
patch("src.agent.langgraph_setup.create_react_agent") as mock_create, \
patch("src.agent.langgraph_setup._fetch_llm_config", new=AsyncMock(return_value=None)), \
patch("src.agent.langgraph_setup.os.getenv", return_value=None):
patch("src.agent.langgraph_setup._fetch_llm_config", new=AsyncMock(return_value=ls._llm_config)):
mock_create.return_value = MagicMock()
await ls.create_agent([])
assert mock_create.call_args[1]["interrupt_before"] == []
@@ -137,13 +133,14 @@ class TestCreateAgent:
async def test_confirm_tools_env_interrupts_before_tools_node(self):
import src.agent.langgraph_setup as ls
ls._llm_config = None
ls.configure_from_api({
"configured": True,
"api_key": "sk-test",
})
with patch("src.agent.langgraph_setup.ChatOpenAI"), \
patch("src.agent.langgraph_setup.create_react_agent") as mock_create, \
patch("src.agent.langgraph_setup._fetch_llm_config", new=AsyncMock(return_value=None)), \
patch("src.agent.langgraph_setup.os.getenv") as mock_getenv:
mock_getenv.side_effect = lambda key, default=None: {
"AGENT_CONFIRM_TOOLS": "true",
}.get(key, default)
patch("src.agent.langgraph_setup._fetch_llm_config", new=AsyncMock(return_value=ls._llm_config)), \
patch("src.agent.langgraph_setup.AGENT_CONFIRM_TOOLS", True):
mock_create.return_value = MagicMock()
await ls.create_agent([])
assert mock_create.call_args[1]["interrupt_before"] == ["tools"]
@@ -153,13 +150,14 @@ class TestCreateAgent:
async def test_uses_env_configured_interrupt_nodes(self):
import src.agent.langgraph_setup as ls
ls._llm_config = None
ls.configure_from_api({
"configured": True,
"api_key": "sk-test",
})
with patch("src.agent.langgraph_setup.ChatOpenAI"), \
patch("src.agent.langgraph_setup.create_react_agent") as mock_create, \
patch("src.agent.langgraph_setup._fetch_llm_config", new=AsyncMock(return_value=None)), \
patch("src.agent.langgraph_setup.os.getenv") as mock_getenv:
mock_getenv.side_effect = lambda key, default=None: {
"AGENT_INTERRUPT_BEFORE": "tools",
}.get(key, default)
patch("src.agent.langgraph_setup._fetch_llm_config", new=AsyncMock(return_value=ls._llm_config)), \
patch("src.agent.langgraph_setup._INTERRUPT_BEFORE", "tools"):
mock_create.return_value = MagicMock()
await ls.create_agent([])
assert mock_create.call_args[1]["interrupt_before"] == ["tools"]
@@ -169,13 +167,14 @@ class TestCreateAgent:
async def test_interrupt_override_bypasses_env_guardrail(self):
import src.agent.langgraph_setup as ls
ls._llm_config = None
ls.configure_from_api({
"configured": True,
"api_key": "sk-test",
})
with patch("src.agent.langgraph_setup.ChatOpenAI"), \
patch("src.agent.langgraph_setup.create_react_agent") as mock_create, \
patch("src.agent.langgraph_setup._fetch_llm_config", new=AsyncMock(return_value=None)), \
patch("src.agent.langgraph_setup.os.getenv") as mock_getenv:
mock_getenv.side_effect = lambda key, default=None: {
"AGENT_CONFIRM_TOOLS": "true",
}.get(key, default)
patch("src.agent.langgraph_setup._fetch_llm_config", new=AsyncMock(return_value=ls._llm_config)), \
patch("src.agent.langgraph_setup.AGENT_CONFIRM_TOOLS", True):
mock_create.return_value = MagicMock()
await ls.create_agent([], interrupt_before=[])
assert mock_create.call_args[1]["interrupt_before"] == []

View File

@@ -113,7 +113,7 @@ class TestFetchLlmConfig:
def test_uses_service_token_header(self):
from src.agent.run import _fetch_llm_config
with patch("src.agent.run.httpx.get") as mock_get, \
patch("src.agent.run.os.getenv", return_value="test-token"):
patch("src.agent.run.SERVICE_JWT", "test-token"):
mock_response = MagicMock()
mock_response.json.return_value = {"configured": True}
mock_get.return_value = mock_response
@@ -135,7 +135,9 @@ class TestMainBlock:
def _run_as_main(self, monkeypatch, env_overrides=None, llm_configured=False,
port_bind_sequence=None, port_always_fail=False):
"""Execute run.py as __main__ with given mocking configuration."""
import importlib
import importlib.util
import os
from pathlib import Path
run_path = Path(__file__).parent.parent.parent / "src" / "agent" / "run.py"
@@ -146,13 +148,23 @@ class TestMainBlock:
for k, v in env_overrides.items():
monkeypatch.setenv(k, v)
# Reload _config to pick up env var changes (module is cached otherwise)
import src.agent._config as agent_config
importlib.reload(agent_config)
svc_jwt = env_overrides.get("SERVICE_JWT", os.environ.get("SERVICE_JWT", ""))
gradio_port = int(env_overrides.get("GRADIO_SERVER_PORT", os.environ.get("GRADIO_SERVER_PORT", "7860")))
gradio_fallback = env_overrides.get("GRADIO_ALLOW_PORT_FALLBACK", os.environ.get("GRADIO_ALLOW_PORT_FALLBACK", "false")).lower() in ("1", "true", "yes")
with patch('httpx.get') as mock_httpx_get, \
patch('socket.socket') as mock_socket_cls, \
patch('asyncio.run') as mock_asyncio_run, \
patch('src.agent.app.create_chat_interface') as mock_create_ci, \
patch('src.agent.context.set_service_jwt') as mock_set_jwt, \
patch('src.agent.langgraph_setup.configure_from_api') as mock_configure, \
patch('src.agent.langgraph_setup.init_checkpointer'):
patch('src.agent.langgraph_setup.init_checkpointer'), \
patch('src.agent.run.SERVICE_JWT', svc_jwt), \
patch('src.agent.run.GRADIO_SERVER_PORT', gradio_port), \
patch('src.agent.run.GRADIO_ALLOW_PORT_FALLBACK', gradio_fallback):
mock_asyncio_run.side_effect = lambda coro: coro.close() if hasattr(coro, "close") else None
# httpx for _fetch_llm_config