Files
ss-tools/backend/src/agent/_tool_resolver.py
busya 12118ac4ec 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
2026-07-01 13:17:29 +03:00

109 lines
5.0 KiB
Python

# backend/src/agent/_tool_resolver.py
# #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.
# 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
# ── Graph nodes — used by confirmation subsystem to distinguish tools from infrastructure ──
_GRAPH_NODE_NAMES = {"agent", "tools", "__start__", "__end__"}
# #region AgentChat.ToolResolver.KnownNames [C:2] [TYPE Function] [SEMANTICS agent-chat,tools,catalog]
# @ingroup AgentChat
# @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:
return set()
# #endregion AgentChat.ToolResolver.KnownNames
# #region AgentChat.ToolResolver.NormalizeArgs [C:2] [TYPE Function] [SEMANTICS agent-chat,tools,args]
# @ingroup AgentChat
# @BRIEF Normalize raw tool arguments into a plain dict regardless of input format.
# @POST Returns dict (empty dict for None/unparseable input).
def normalize_tool_args(raw_args: Any) -> dict[str, Any]:
if raw_args is None:
return {}
if isinstance(raw_args, dict):
return raw_args
if hasattr(raw_args, "model_dump"):
dumped = raw_args.model_dump()
return dumped if isinstance(dumped, dict) else {}
if hasattr(raw_args, "dict"):
dumped = raw_args.dict()
return dumped if isinstance(dumped, dict) else {}
return {}
# #endregion AgentChat.ToolResolver.NormalizeArgs
# #region AgentChat.ToolResolver.CoerceToolCall [C:2] [TYPE Function] [SEMANTICS agent-chat,tools,coerce]
# @ingroup AgentChat
# @BRIEF Extract (tool_name, tool_args) tuple from a dict or object tool call.
# @POST Returns (name, args) tuple; name may be None if unparseable.
def coerce_tool_call(tool_call: Any) -> tuple[str | None, dict[str, Any]]:
if isinstance(tool_call, dict):
return (
tool_call.get("name") or tool_call.get("tool") or tool_call.get("id"),
normalize_tool_args(tool_call.get("args") or tool_call.get("input")),
)
return (
getattr(tool_call, "name", None),
normalize_tool_args(getattr(tool_call, "args", None)),
)
# #endregion AgentChat.ToolResolver.CoerceToolCall
# #region AgentChat.ToolResolver.ExtractFromState [C:2] [TYPE Function] [SEMANTICS agent-chat,tools,checkpoint]
# @ingroup AgentChat
# @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:
messages = (state.values.get("messages") if hasattr(state, "values") else []) or []
for msg in reversed(messages[-5:]):
if hasattr(msg, "tool_calls") and msg.tool_calls:
tool_name, tool_args = coerce_tool_call(msg.tool_calls[0])
if tool_name:
return (str(tool_name), tool_args)
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, {})
return (None, {})
# #endregion AgentChat.ToolResolver.ExtractFromState
# #region AgentChat.ToolResolver.FindTool [C:2] [TYPE Function] [SEMANTICS agent-chat,tools,lookup]
# @ingroup AgentChat
# @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
# #endregion AgentChat.ToolResolver