fix: harden agent startup and websocket auth

This commit is contained in:
root
2026-07-27 11:49:18 +03:00
parent a386a1fd5c
commit 3fd8525c4e
10 changed files with 283 additions and 29 deletions

View File

@@ -34,7 +34,8 @@ openpyxl
# DB for LangGraph checkpoint
psycopg2-binary
psycopg>=3.1
# Bundle libpq with the agent environment; plain psycopg requires an OS-level libpq.
psycopg[binary]>=3.1
# Retry/utility
tenacity>=8.0.0

View File

@@ -9,7 +9,7 @@
import inspect as _inspect
import os
from urllib.parse import urlsplit
from urllib.parse import urlsplit, urlunsplit
from langchain_openai import ChatOpenAI
from langgraph.checkpoint.memory import InMemorySaver
@@ -56,20 +56,91 @@ _CHECKPOINTER_INIT = False
_CHECKPOINTER_CONN = None
# #region AgentChat.LangGraph.Setup.InitCheckpointer [C:3] [TYPE Function] [SEMANTICS agent-chat,langgraph,checkpointer,postgres]
# #region AgentChat.LangGraph.Setup.RedactDbUrl [C:2] [TYPE Function] [SEMANTICS agent-chat,diagnostics,security]
# @ingroup AgentChat
# @BRIEF Initialize AsyncPostgresSaver from DATABASE_URL env var.
# @BRIEF Redact password from a database URL for safe diagnostic logging.
# @INVARIANT Never returns raw password or full credentials in the output string.
def _redact_db_url(url: str) -> str:
"""Return a copy of `url` with the password replaced by '***'."""
if not url:
return "<empty>"
try:
parsed = urlsplit(url)
if not parsed.scheme or not parsed.hostname:
return "<invalid-url>"
host = f"[{parsed.hostname}]" if ":" in parsed.hostname else parsed.hostname
port = f":{parsed.port}" if parsed.port else ""
user = f"{parsed.username}:***@" if parsed.username else ""
return urlunsplit((parsed.scheme, f"{user}{host}{port}", parsed.path, "", ""))
except Exception:
return "<unparseable-url>"
# #endregion AgentChat.LangGraph.Setup.RedactDbUrl
# #region AgentChat.LangGraph.Setup.InitCheckpointer [C:4] [TYPE Function] [SEMANTICS agent-chat,langgraph,checkpointer,postgres]
# @ingroup AgentChat
# @BRIEF Initialize AsyncPostgresSaver from DATABASE_URL env var with validation and diagnostics.
# @SIDE_EFFECT Connects to PostgreSQL; creates checkpointer table via setup().
# @INVARIANT DATABASE_URL must be a valid PostgreSQL URI before connection is attempted.
# @RATIONALE Production agent crash-loop was caused by an empty or misconfigured DATABASE_URL
# that resolved to a nonexistent Unix socket. Pre-connection validation + redacted
# diagnostics (host, port, db name — never password) allows operators to debug
# configuration failures without exposing secrets in logs.
async def init_checkpointer() -> None:
global _CHECKPOINTER, _CHECKPOINTER_INIT, _CHECKPOINTER_CONN
if _CHECKPOINTER_INIT:
return
db_url = os.getenv("DATABASE_URL")
pg_url = db_url.replace("postgresql+psycopg2://", "postgres://").replace("postgresql://", "postgres://")
_CHECKPOINTER_CONN = await psycopg.AsyncConnection.connect(pg_url, autocommit=True, row_factory=dict_row)
_CHECKPOINTER = AsyncPostgresSaver(_CHECKPOINTER_CONN)
await _CHECKPOINTER.setup()
if not db_url or not db_url.strip():
logger.explore(
"DATABASE_URL env var is missing or empty — checkpointer cannot be initialized",
payload={"env_var": "DATABASE_URL"},
error="DATABASE_URL is not set",
)
raise RuntimeError("DATABASE_URL is not set. PostgreSQL checkpointer requires a valid database URI.")
# Redact password from URL for safe diagnostic logging.
_sanitized = _redact_db_url(db_url)
logger.reason(
"Initializing PostgreSQL checkpointer",
payload={"sanitized_url": _sanitized},
)
pg_url: str = db_url.replace("postgresql+psycopg2://", "postgres://").replace("postgresql://", "postgres://")
if not pg_url.startswith("postgres://") and not pg_url.startswith("postgresql://"):
logger.explore(
"DATABASE_URL does not look like a PostgreSQL connection string",
payload={"sanitized_url": _sanitized, "parsed_scheme": pg_url.split("://")[0] if "://" in pg_url else "none"},
error="Not a PostgreSQL URL",
)
raise RuntimeError(f"DATABASE_URL must be a PostgreSQL URI. Got invalid scheme.")
try:
_CHECKPOINTER_CONN = await psycopg.AsyncConnection.connect(pg_url, autocommit=True, row_factory=dict_row)
except Exception as e:
logger.explore(
"Failed to connect to PostgreSQL checkpointer",
payload={"sanitized_url": _sanitized, "exception_type": type(e).__name__},
error="PostgreSQL connection failed; inspect database service availability and credentials",
)
raise
try:
_CHECKPOINTER = AsyncPostgresSaver(_CHECKPOINTER_CONN)
await _CHECKPOINTER.setup()
except Exception as e:
logger.explore(
"Checkpointer setup() failed",
payload={"sanitized_url": _sanitized, "exception_type": type(e).__name__},
error="PostgreSQL checkpointer setup failed; inspect database schema permissions",
)
raise
_CHECKPOINTER_INIT = True
logger.reason(
"PostgreSQL checkpointer initialized successfully",
payload={"sanitized_url": _sanitized},
)
# #endregion AgentChat.LangGraph.Setup.InitCheckpointer
_llm_config: dict | None = None

View File

@@ -64,6 +64,44 @@ def test_llm_diagnostics_redacts_api_key_and_path():
# #endregion Test.AgentChat.TestLlmDiagnostics
# #region Test.AgentChat.TestCheckpointerDiagnostics [C:3] [TYPE Class] [SEMANTICS test,agent,checkpointer,security]
# @BRIEF Verify checkpointer diagnostics redact credentials and reject invalid configuration safely.
# @RELATION BINDS_TO -> [AgentChat.LangGraph.Setup.InitCheckpointer]
class TestCheckpointerDiagnostics:
def test_redact_db_url_removes_password_query_and_fragment(self):
from ss_tools.agent.langgraph_setup import _redact_db_url
redacted = _redact_db_url("postgresql://user:secret@[::1]:5432/ss_tools?sslpassword=hidden#fragment")
assert redacted == "postgresql://user:***@[::1]:5432/ss_tools"
assert "secret" not in redacted
assert "hidden" not in redacted
def test_redact_db_url_handles_unparseable_value(self):
from ss_tools.agent.langgraph_setup import _redact_db_url
assert _redact_db_url("not a database url") == "<invalid-url>"
@pytest.mark.anyio
async def test_init_checkpointer_rejects_missing_database_url(self, monkeypatch):
import ss_tools.agent.langgraph_setup as ls
monkeypatch.delenv("DATABASE_URL", raising=False)
ls._CHECKPOINTER_INIT = False
with pytest.raises(RuntimeError, match="DATABASE_URL is not set"):
await ls.init_checkpointer()
@pytest.mark.anyio
async def test_init_checkpointer_redacts_connection_failure(self, monkeypatch):
import ss_tools.agent.langgraph_setup as ls
monkeypatch.setenv("DATABASE_URL", "postgresql://agent:secret@db:5432/ss_tools?sslpassword=hidden")
ls._CHECKPOINTER_INIT = False
with patch("ss_tools.agent.langgraph_setup.psycopg.AsyncConnection.connect", new=AsyncMock(side_effect=Exception("secret"))):
with pytest.raises(Exception, match="secret"):
await ls.init_checkpointer()
# #endregion Test.AgentChat.TestCheckpointerDiagnostics
# #region Test.AgentChat.TestCreateAgent [C:2] [TYPE Function]
# @BRIEF Test create_agent with various LLM config states.
class TestCreateAgent: