fix(security): resolve Critical+High findings from module audit — agent, translate, superset_client

P0 — CRITICAL (CWE-798): JWT_SECRET crash-early
  Replace hardcoded super-secret-key fallback with os.environ["JWT_SECRET"]
  and ${JWT_SECRET:?} syntax in app.py + docker-compose files

P1 — HIGH: Frontend dependency CVEs
  Upgrade svelte 5.43.8 → 5.56.4 — resolves devalue DoS (GHSA-g2pg-6438-jwpf)
  and svelte XSS (GHSA-crpf-4hrx-3jrp, GHSA-m56q-vw4c-c2cp, GHSA-rcqx-6q8c-2c42)

P2 — MEDIUM: Logging hygiene + contract gaps + tool resolver refactor
  Apply _redact_sensitive_fields() in middleware + event streaming
  Truncate LLM error body to 100 chars
  Add @RATIONALE/@REJECTED to HandleResume + SaveConversation
  Refactor deterministic intent matching → LLM-driven tool resolution

P3 — LOW: Translate logging hardening
  Move _sanitize_url() to _utils.py (shared, no circular imports)
  Sanitize base_url before logging in _llm_call.py and _llm_async_http.py
  Emit EXPLORE warning when LLM_SSL_VERIFY=false disables TLS

superset_client module: passed clean — no changes needed
This commit is contained in:
2026-07-01 13:17:29 +03:00
parent 1e24452b1a
commit 12118ac4ec
21 changed files with 871 additions and 336 deletions

View File

@@ -1,79 +1,36 @@
# backend/src/agent/_tool_resolver.py
# #region AgentChat.ToolResolver [C:3] [TYPE Module] [SEMANTICS agent-chat,tools,classification,resolution]
# @defgroup AgentChat Tool classification constants and resolution helpers for the LangGraph agent.
# #region AgentChat.ToolResolver [C:2] [TYPE Module] [SEMANTICS agent-chat,tools,resolution]
# @defgroup AgentChat Tool resolution helpers for the LangGraph agent.
# @LAYER Service
# @RELATION DEPENDS_ON -> [AgentChat.Tools]
# @RATIONALE Centralised tool resolution prevents duplication of tool-name matching logic across
# the handler and confirmation subsystems. Single source of truth for tool risk classification.
# @RATIONALE Centralised tool resolution prevents duplication of tool-name matching logic.
# Deterministic intent matching (infer_tool_from_text, fast_confirmation_tool,
# keyword lists, negation guard, classification sets) removed — LLM handles
# all intent detection through LangGraph tool-calling. Only utility helpers
# (tool call coercion, args normalization) remain.
# @REJECTED Deterministic intent matching — fragile substring collisions, maintenance burden
# of 24 keyword lists across 3 files, negation blindness in fast-track, and
# inability to handle synonyms ("панели"≠"дашборды") or typos ("дашборд").
# @REJECTED Fast-track confirmation — bypasses LLM, causing negation blindness.
# @REJECTED Tool risk classification sets — LLM decides which tools to call;
# LangGraph interrupt_before handles HITL for dangerous tools at graph level.
from typing import Any
from src.agent.tools import get_all_tools
from src.core.logger import logger
# #region AgentChat.ToolResolver.Sets [C:1] [TYPE Constants] [SEMANTICS agent-chat,tools,sets]
# @ingroup AgentChat
# @BRIEF Tool classification sets — safe (read-only), guarded (write), dangerous (deploy).
_SAFE_AGENT_TOOLS = {
"show_capabilities",
"search_dashboards",
"get_health_summary",
"list_environments",
"get_task_status",
"list_llm_providers",
"get_llm_status",
"list_maintenance_events",
# NEW: read-only Superset tools
"superset_execute_sql",
"superset_explore_database",
"superset_audit_permissions",
"superset_format_sql",
}
_GUARDED_AGENT_TOOLS = {
"create_branch",
"commit_changes",
"execute_migration",
"run_backup",
"run_llm_validation",
"run_llm_documentation",
"start_maintenance",
"end_maintenance",
# NEW: guarded Superset write operations
"superset_create_dashboard",
"superset_copy_dashboard",
"superset_create_dataset",
}
_DANGEROUS_AGENT_TOOLS = {
"deploy_dashboard",
}
# ── Graph nodes — used by confirmation subsystem to distinguish tools from infrastructure ──
_GRAPH_NODE_NAMES = {"agent", "tools", "__start__", "__end__"}
_FAST_CONFIRM_TOOLS = {
"show_capabilities",
"list_environments",
"list_llm_providers",
"get_llm_status",
"list_maintenance_events",
"superset_explore_database",
"superset_audit_permissions",
"superset_format_sql",
}
# #endregion AgentChat.ToolResolver.Sets
# #region AgentChat.ToolResolver.KnownNames [C:2] [TYPE Function] [SEMANTICS agent-chat,tools,catalog]
# @ingroup AgentChat
# @BRIEF Return registered LangChain tool names without letting helper failures break HITL UX.
# @POST Returns set of tool name strings; falls back to static union on failure.
# @BRIEF Return registered LangChain tool names.
# @POST Returns set of tool name strings; falls back to empty set on failure.
def known_agent_tool_names() -> set[str]:
try:
from src.agent.tools import get_all_tools
return {str(tool_obj.name) for tool_obj in get_all_tools() if getattr(tool_obj, "name", None)}
except Exception as exc:
logger.explore(
"tool catalog lookup failed",
payload={"error": str(exc)},
extra={"src": "AgentChat.ToolResolver"},
)
return _SAFE_AGENT_TOOLS | _GUARDED_AGENT_TOOLS | _DANGEROUS_AGENT_TOOLS
except Exception:
return set()
# #endregion AgentChat.ToolResolver.KnownNames
@@ -113,61 +70,11 @@ def coerce_tool_call(tool_call: Any) -> tuple[str | None, dict[str, Any]]:
# #endregion AgentChat.ToolResolver.CoerceToolCall
# #region AgentChat.ToolResolver.InferFromText [C:3] [TYPE Function] [SEMANTICS agent-chat,tools,inference]
# #region AgentChat.ToolResolver.ExtractFromState [C:2] [TYPE Function] [SEMANTICS agent-chat,tools,checkpoint]
# @ingroup AgentChat
# @BRIEF Infer which tool the user likely wants based on keywords in the message text.
# @POST Returns tool name string, or None if no match.
# @RATIONALE Some LLMs fail to emit tool calls even when instructed. This fallback
# uses keyword matching to guess the user's intent and auto-trigger HITL.
def infer_tool_from_text(text: str) -> str | None:
lowered = (text or "").lower()
inferred: str | None = None
if any(word in lowered for word in ["окруж", "environment", "env"]):
inferred = "list_environments"
elif any(word in lowered for word in ["maintenance", "обслуж", "баннер"]):
if any(word in lowered for word in ["start", "созда", "запусти", "начни"]):
inferred = "start_maintenance"
elif any(word in lowered for word in ["end", "закрой", "заверши", "останов"]):
inferred = "end_maintenance"
else:
inferred = "list_maintenance_events"
elif any(word in lowered for word in ["дашборд", "dashboard", "dashboards", "дашборды"]):
inferred = "search_dashboards"
elif any(word in lowered for word in ["здоров", "health", "статус системы", "system status"]):
inferred = "get_health_summary"
elif any(word in lowered for word in ["задач", "task", "таск"]):
inferred = "get_task_status"
elif any(word in lowered for word in ["llm", "provider", "провайдер", "модель"]):
inferred = "list_llm_providers"
elif any(word in lowered for word in ["branch", "ветк"]):
inferred = "create_branch"
elif any(word in lowered for word in ["commit", "коммит"]):
inferred = "commit_changes"
elif any(word in lowered for word in ["deploy", "депло", "разверн"]):
inferred = "deploy_dashboard"
elif any(word in lowered for word in ["миграц", "migration", "migrate"]):
inferred = "execute_migration"
elif any(word in lowered for word in ["backup", "бэкап", "резерв"]):
inferred = "run_backup"
elif any(word in lowered for word in ["валидац", "validation", "validate"]):
inferred = "run_llm_validation"
elif any(word in lowered for word in ["документ", "documentation", "docs"]):
inferred = "run_llm_documentation"
elif any(word in lowered for word in ["инструмент", "tool", "capabilit", "умеешь", "можешь"]):
inferred = "show_capabilities"
if inferred:
logger.reason("Tool inferred from user text",
payload={"tool": inferred, "text_preview": (text or "")[:80]},
extra={"src": "AgentChat.ToolResolver.InferFromText"})
return inferred
# #endregion AgentChat.ToolResolver.InferFromText
# #region AgentChat.ToolResolver.ExtractFromState [C:3] [TYPE Function] [SEMANTICS agent-chat,tools,checkpoint]
# @ingroup AgentChat
# @BRIEF Extract pending tool name and args from the LangGraph checkpoint; infer only as last resort.
# @BRIEF Extract pending tool name and args from the LangGraph checkpoint.
# @POST Returns (tool_name, args) tuple; (None, {}) if nothing found.
# @RATIONALE LLM handles intent — no fallback to keyword inference.
def extract_tool_call_from_state(state, user_text: str = "") -> tuple[str | None, dict[str, Any]]:
known_tools = known_agent_tool_names()
try:
@@ -177,26 +84,14 @@ def extract_tool_call_from_state(state, user_text: str = "") -> tuple[str | None
tool_name, tool_args = coerce_tool_call(msg.tool_calls[0])
if tool_name:
return (str(tool_name), tool_args)
except Exception as exc:
logger.explore(
"tool_call extraction failed",
payload={"error": str(exc)},
extra={"src": "AgentChat.ToolResolver"},
)
except Exception:
pass
if getattr(state, "next", None):
node_or_tool = str(state.next[0])
if node_or_tool in known_tools and node_or_tool not in _GRAPH_NODE_NAMES:
return (node_or_tool, {})
inferred_tool = infer_tool_from_text(user_text)
if inferred_tool:
logger.explore(
"tool_call inferred from user text",
payload={"tool": inferred_tool},
extra={"src": "AgentChat.ToolResolver"},
)
return (inferred_tool, {})
return (None, {})
# #endregion AgentChat.ToolResolver.ExtractFromState
@@ -206,16 +101,8 @@ def extract_tool_call_from_state(state, user_text: str = "") -> tuple[str | None
# @BRIEF Find a registered LangChain tool by name.
# @POST Returns tool object or None if not found.
def find_tool(tool_name: str):
from src.agent.tools import get_all_tools
return next((tool_obj for tool_obj in get_all_tools() if getattr(tool_obj, "name", None) == tool_name), None)
# #endregion AgentChat.ToolResolver.FindTool
# #region AgentChat.ToolResolver.FastConfirm [C:2] [TYPE Function] [SEMANTICS agent-chat,tools,fast-path]
# @ingroup AgentChat
# @BRIEF Check if user text maps to a tool eligible for fast-track HITL confirmation.
# @POST Returns tool name if match, None otherwise.
def fast_confirmation_tool(text: str) -> str | None:
tool_name = infer_tool_from_text(text)
return tool_name if tool_name in _FAST_CONFIRM_TOOLS else None
# #endregion AgentChat.ToolResolver.FastConfirm
# #endregion AgentChat.ToolResolver