lfx-nightly 0.2.0.dev0__py3-none-any.whl → 0.2.0.dev26__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (188) hide show
  1. lfx/_assets/component_index.json +1 -1
  2. lfx/base/agents/agent.py +13 -1
  3. lfx/base/agents/altk_base_agent.py +380 -0
  4. lfx/base/agents/altk_tool_wrappers.py +565 -0
  5. lfx/base/agents/events.py +2 -1
  6. lfx/base/composio/composio_base.py +159 -224
  7. lfx/base/data/base_file.py +88 -21
  8. lfx/base/data/storage_utils.py +192 -0
  9. lfx/base/data/utils.py +178 -14
  10. lfx/base/embeddings/embeddings_class.py +113 -0
  11. lfx/base/models/groq_constants.py +74 -58
  12. lfx/base/models/groq_model_discovery.py +265 -0
  13. lfx/base/models/model.py +1 -1
  14. lfx/base/models/model_utils.py +100 -0
  15. lfx/base/models/openai_constants.py +7 -0
  16. lfx/base/models/watsonx_constants.py +32 -8
  17. lfx/base/tools/run_flow.py +601 -129
  18. lfx/cli/commands.py +6 -3
  19. lfx/cli/common.py +2 -2
  20. lfx/cli/run.py +1 -1
  21. lfx/cli/script_loader.py +53 -11
  22. lfx/components/Notion/create_page.py +1 -1
  23. lfx/components/Notion/list_database_properties.py +1 -1
  24. lfx/components/Notion/list_pages.py +1 -1
  25. lfx/components/Notion/list_users.py +1 -1
  26. lfx/components/Notion/page_content_viewer.py +1 -1
  27. lfx/components/Notion/search.py +1 -1
  28. lfx/components/Notion/update_page_property.py +1 -1
  29. lfx/components/__init__.py +19 -5
  30. lfx/components/{agents → altk}/__init__.py +5 -9
  31. lfx/components/altk/altk_agent.py +193 -0
  32. lfx/components/apify/apify_actor.py +1 -1
  33. lfx/components/composio/__init__.py +70 -18
  34. lfx/components/composio/apollo_composio.py +11 -0
  35. lfx/components/composio/bitbucket_composio.py +11 -0
  36. lfx/components/composio/canva_composio.py +11 -0
  37. lfx/components/composio/coda_composio.py +11 -0
  38. lfx/components/composio/composio_api.py +10 -0
  39. lfx/components/composio/discord_composio.py +1 -1
  40. lfx/components/composio/elevenlabs_composio.py +11 -0
  41. lfx/components/composio/exa_composio.py +11 -0
  42. lfx/components/composio/firecrawl_composio.py +11 -0
  43. lfx/components/composio/fireflies_composio.py +11 -0
  44. lfx/components/composio/gmail_composio.py +1 -1
  45. lfx/components/composio/googlebigquery_composio.py +11 -0
  46. lfx/components/composio/googlecalendar_composio.py +1 -1
  47. lfx/components/composio/googledocs_composio.py +1 -1
  48. lfx/components/composio/googlemeet_composio.py +1 -1
  49. lfx/components/composio/googlesheets_composio.py +1 -1
  50. lfx/components/composio/googletasks_composio.py +1 -1
  51. lfx/components/composio/heygen_composio.py +11 -0
  52. lfx/components/composio/mem0_composio.py +11 -0
  53. lfx/components/composio/peopledatalabs_composio.py +11 -0
  54. lfx/components/composio/perplexityai_composio.py +11 -0
  55. lfx/components/composio/serpapi_composio.py +11 -0
  56. lfx/components/composio/slack_composio.py +3 -574
  57. lfx/components/composio/slackbot_composio.py +1 -1
  58. lfx/components/composio/snowflake_composio.py +11 -0
  59. lfx/components/composio/tavily_composio.py +11 -0
  60. lfx/components/composio/youtube_composio.py +2 -2
  61. lfx/components/cuga/__init__.py +34 -0
  62. lfx/components/cuga/cuga_agent.py +730 -0
  63. lfx/components/data/__init__.py +78 -28
  64. lfx/components/data_source/__init__.py +58 -0
  65. lfx/components/{data → data_source}/api_request.py +26 -3
  66. lfx/components/{data → data_source}/csv_to_data.py +15 -10
  67. lfx/components/{data → data_source}/json_to_data.py +15 -8
  68. lfx/components/{data → data_source}/news_search.py +1 -1
  69. lfx/components/{data → data_source}/rss.py +1 -1
  70. lfx/components/{data → data_source}/sql_executor.py +1 -1
  71. lfx/components/{data → data_source}/url.py +1 -1
  72. lfx/components/{data → data_source}/web_search.py +1 -1
  73. lfx/components/datastax/astradb_cql.py +1 -1
  74. lfx/components/datastax/astradb_graph.py +1 -1
  75. lfx/components/datastax/astradb_tool.py +1 -1
  76. lfx/components/datastax/astradb_vectorstore.py +1 -1
  77. lfx/components/datastax/hcd.py +1 -1
  78. lfx/components/deactivated/json_document_builder.py +1 -1
  79. lfx/components/docling/__init__.py +0 -3
  80. lfx/components/elastic/elasticsearch.py +1 -1
  81. lfx/components/elastic/opensearch_multimodal.py +1575 -0
  82. lfx/components/files_and_knowledge/__init__.py +47 -0
  83. lfx/components/{data → files_and_knowledge}/directory.py +1 -1
  84. lfx/components/{data → files_and_knowledge}/file.py +246 -18
  85. lfx/components/{knowledge_bases → files_and_knowledge}/retrieval.py +2 -2
  86. lfx/components/{data → files_and_knowledge}/save_file.py +142 -22
  87. lfx/components/flow_controls/__init__.py +58 -0
  88. lfx/components/{logic → flow_controls}/conditional_router.py +1 -1
  89. lfx/components/{logic → flow_controls}/loop.py +43 -9
  90. lfx/components/flow_controls/run_flow.py +108 -0
  91. lfx/components/glean/glean_search_api.py +1 -1
  92. lfx/components/groq/groq.py +35 -28
  93. lfx/components/helpers/__init__.py +102 -0
  94. lfx/components/input_output/__init__.py +3 -1
  95. lfx/components/input_output/chat.py +4 -3
  96. lfx/components/input_output/chat_output.py +4 -4
  97. lfx/components/input_output/text.py +1 -1
  98. lfx/components/input_output/text_output.py +1 -1
  99. lfx/components/{data → input_output}/webhook.py +1 -1
  100. lfx/components/knowledge_bases/__init__.py +59 -4
  101. lfx/components/langchain_utilities/character.py +1 -1
  102. lfx/components/langchain_utilities/csv_agent.py +84 -16
  103. lfx/components/langchain_utilities/json_agent.py +67 -12
  104. lfx/components/langchain_utilities/language_recursive.py +1 -1
  105. lfx/components/llm_operations/__init__.py +46 -0
  106. lfx/components/{processing → llm_operations}/batch_run.py +1 -1
  107. lfx/components/{processing → llm_operations}/lambda_filter.py +1 -1
  108. lfx/components/{logic → llm_operations}/llm_conditional_router.py +1 -1
  109. lfx/components/{processing/llm_router.py → llm_operations/llm_selector.py} +3 -3
  110. lfx/components/{processing → llm_operations}/structured_output.py +1 -1
  111. lfx/components/logic/__init__.py +126 -0
  112. lfx/components/mem0/mem0_chat_memory.py +11 -0
  113. lfx/components/models/__init__.py +64 -9
  114. lfx/components/models_and_agents/__init__.py +49 -0
  115. lfx/components/{agents → models_and_agents}/agent.py +2 -2
  116. lfx/components/models_and_agents/embedding_model.py +423 -0
  117. lfx/components/models_and_agents/language_model.py +398 -0
  118. lfx/components/{agents → models_and_agents}/mcp_component.py +53 -44
  119. lfx/components/{helpers → models_and_agents}/memory.py +1 -1
  120. lfx/components/nvidia/system_assist.py +1 -1
  121. lfx/components/olivya/olivya.py +1 -1
  122. lfx/components/ollama/ollama.py +17 -3
  123. lfx/components/processing/__init__.py +9 -57
  124. lfx/components/processing/converter.py +1 -1
  125. lfx/components/processing/dataframe_operations.py +1 -1
  126. lfx/components/processing/parse_json_data.py +2 -2
  127. lfx/components/processing/parser.py +1 -1
  128. lfx/components/processing/split_text.py +1 -1
  129. lfx/components/qdrant/qdrant.py +1 -1
  130. lfx/components/redis/redis.py +1 -1
  131. lfx/components/twelvelabs/split_video.py +10 -0
  132. lfx/components/twelvelabs/video_file.py +12 -0
  133. lfx/components/utilities/__init__.py +43 -0
  134. lfx/components/{helpers → utilities}/calculator_core.py +1 -1
  135. lfx/components/{helpers → utilities}/current_date.py +1 -1
  136. lfx/components/{processing → utilities}/python_repl_core.py +1 -1
  137. lfx/components/vectorstores/local_db.py +9 -0
  138. lfx/components/youtube/youtube_transcripts.py +118 -30
  139. lfx/custom/custom_component/component.py +57 -1
  140. lfx/custom/custom_component/custom_component.py +68 -6
  141. lfx/graph/edge/base.py +43 -20
  142. lfx/graph/graph/base.py +4 -1
  143. lfx/graph/state/model.py +15 -2
  144. lfx/graph/utils.py +6 -0
  145. lfx/graph/vertex/base.py +4 -1
  146. lfx/graph/vertex/param_handler.py +10 -7
  147. lfx/helpers/__init__.py +12 -0
  148. lfx/helpers/flow.py +117 -0
  149. lfx/inputs/input_mixin.py +24 -1
  150. lfx/inputs/inputs.py +13 -1
  151. lfx/interface/components.py +161 -83
  152. lfx/log/logger.py +5 -3
  153. lfx/services/database/__init__.py +5 -0
  154. lfx/services/database/service.py +25 -0
  155. lfx/services/deps.py +87 -22
  156. lfx/services/manager.py +19 -6
  157. lfx/services/mcp_composer/service.py +998 -157
  158. lfx/services/session.py +5 -0
  159. lfx/services/settings/base.py +51 -7
  160. lfx/services/settings/constants.py +8 -0
  161. lfx/services/storage/local.py +76 -46
  162. lfx/services/storage/service.py +152 -29
  163. lfx/template/field/base.py +3 -0
  164. lfx/utils/ssrf_protection.py +384 -0
  165. lfx/utils/validate_cloud.py +26 -0
  166. {lfx_nightly-0.2.0.dev0.dist-info → lfx_nightly-0.2.0.dev26.dist-info}/METADATA +38 -22
  167. {lfx_nightly-0.2.0.dev0.dist-info → lfx_nightly-0.2.0.dev26.dist-info}/RECORD +182 -150
  168. {lfx_nightly-0.2.0.dev0.dist-info → lfx_nightly-0.2.0.dev26.dist-info}/WHEEL +1 -1
  169. lfx/components/agents/altk_agent.py +0 -366
  170. lfx/components/agents/cuga_agent.py +0 -1013
  171. lfx/components/docling/docling_remote_vlm.py +0 -284
  172. lfx/components/logic/run_flow.py +0 -71
  173. lfx/components/models/embedding_model.py +0 -195
  174. lfx/components/models/language_model.py +0 -144
  175. /lfx/components/{data → data_source}/mock_data.py +0 -0
  176. /lfx/components/{knowledge_bases → files_and_knowledge}/ingestion.py +0 -0
  177. /lfx/components/{logic → flow_controls}/data_conditional_router.py +0 -0
  178. /lfx/components/{logic → flow_controls}/flow_tool.py +0 -0
  179. /lfx/components/{logic → flow_controls}/listen.py +0 -0
  180. /lfx/components/{logic → flow_controls}/notify.py +0 -0
  181. /lfx/components/{logic → flow_controls}/pass_message.py +0 -0
  182. /lfx/components/{logic → flow_controls}/sub_flow.py +0 -0
  183. /lfx/components/{processing → models_and_agents}/prompt.py +0 -0
  184. /lfx/components/{helpers → processing}/create_list.py +0 -0
  185. /lfx/components/{helpers → processing}/output_parser.py +0 -0
  186. /lfx/components/{helpers → processing}/store_message.py +0 -0
  187. /lfx/components/{helpers → utilities}/id_generator.py +0 -0
  188. {lfx_nightly-0.2.0.dev0.dist-info → lfx_nightly-0.2.0.dev26.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,384 @@
1
+ """SSRF (Server-Side Request Forgery) protection utilities.
2
+
3
+ This module provides validation to prevent SSRF attacks by blocking requests to:
4
+ - Private IP ranges (RFC 1918)
5
+ - Loopback addresses
6
+ - Cloud metadata endpoints (169.254.169.254)
7
+ - Other internal/special-use addresses
8
+
9
+ IMPORTANT: HTTP Redirects
10
+ According to OWASP SSRF Prevention Cheat Sheet, HTTP redirects should be DISABLED
11
+ to prevent bypass attacks where a public URL redirects to internal resources.
12
+ The API Request component has (as of v1.7.0) follow_redirects=False by default.
13
+ See: https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html
14
+
15
+ Configuration:
16
+ LANGFLOW_SSRF_PROTECTION_ENABLED: Enable/disable SSRF protection (default: false)
17
+ TODO: Change default to true in next major version (2.0)
18
+ LANGFLOW_SSRF_ALLOWED_HOSTS: Comma-separated list of allowed hosts/CIDR ranges
19
+ Examples: "192.168.1.0/24,internal-api.company.local,10.0.0.5"
20
+
21
+ TODO: In next major version (2.0):
22
+ - Change LANGFLOW_SSRF_PROTECTION_ENABLED default to "true"
23
+ - Remove warning-only mode and enforce blocking
24
+ - Update documentation to reflect breaking change
25
+ """
26
+
27
+ import functools
28
+ import ipaddress
29
+ import socket
30
+ from urllib.parse import urlparse
31
+
32
+ from lfx.logging import logger
33
+ from lfx.services.deps import get_settings_service
34
+
35
+
36
+ class SSRFProtectionError(ValueError):
37
+ """Raised when a URL is blocked due to SSRF protection."""
38
+
39
+
40
+ @functools.cache
41
+ def get_blocked_ip_ranges() -> list[ipaddress.IPv4Network | ipaddress.IPv6Network]:
42
+ """Get the list of blocked IP ranges, initializing lazily on first access.
43
+
44
+ This lazy loading avoids the startup cost of creating all ip_network objects
45
+ at module import time.
46
+
47
+ Returns:
48
+ list: List of blocked IPv4 and IPv6 network ranges.
49
+ """
50
+ return [
51
+ # IPv4 ranges
52
+ ipaddress.ip_network("0.0.0.0/8"), # Current network (only valid as source)
53
+ ipaddress.ip_network("10.0.0.0/8"), # Private network (RFC 1918)
54
+ ipaddress.ip_network("100.64.0.0/10"), # Carrier-grade NAT (RFC 6598)
55
+ ipaddress.ip_network("127.0.0.0/8"), # Loopback
56
+ ipaddress.ip_network("169.254.0.0/16"), # Link-local / AWS metadata
57
+ ipaddress.ip_network("172.16.0.0/12"), # Private network (RFC 1918)
58
+ ipaddress.ip_network("192.0.0.0/24"), # IETF Protocol Assignments
59
+ ipaddress.ip_network("192.0.2.0/24"), # Documentation (TEST-NET-1)
60
+ ipaddress.ip_network("192.168.0.0/16"), # Private network (RFC 1918)
61
+ ipaddress.ip_network("198.18.0.0/15"), # Benchmarking
62
+ ipaddress.ip_network("198.51.100.0/24"), # Documentation (TEST-NET-2)
63
+ ipaddress.ip_network("203.0.113.0/24"), # Documentation (TEST-NET-3)
64
+ ipaddress.ip_network("224.0.0.0/4"), # Multicast
65
+ ipaddress.ip_network("240.0.0.0/4"), # Reserved
66
+ ipaddress.ip_network("255.255.255.255/32"), # Broadcast
67
+ # IPv6 ranges
68
+ ipaddress.ip_network("::1/128"), # Loopback
69
+ ipaddress.ip_network("::/128"), # Unspecified address
70
+ ipaddress.ip_network("::ffff:0:0/96"), # IPv4-mapped IPv6 addresses
71
+ ipaddress.ip_network("100::/64"), # Discard prefix
72
+ ipaddress.ip_network("2001::/23"), # IETF Protocol Assignments
73
+ ipaddress.ip_network("2001:db8::/32"), # Documentation
74
+ ipaddress.ip_network("fc00::/7"), # Unique local addresses (ULA)
75
+ ipaddress.ip_network("fe80::/10"), # Link-local
76
+ ipaddress.ip_network("ff00::/8"), # Multicast
77
+ ]
78
+
79
+
80
+ def is_ssrf_protection_enabled() -> bool:
81
+ """Check if SSRF protection is enabled in settings.
82
+
83
+ Returns:
84
+ bool: True if SSRF protection is enabled, False otherwise.
85
+ """
86
+ return get_settings_service().settings.ssrf_protection_enabled
87
+
88
+
89
+ def get_allowed_hosts() -> list[str]:
90
+ """Get list of allowed hosts and/or CIDR ranges for SSRF protection.
91
+
92
+ Returns:
93
+ list[str]: Stripped hostnames or CIDR blocks from settings, or empty list if unset.
94
+ """
95
+ allowed_hosts = get_settings_service().settings.ssrf_allowed_hosts
96
+ if not allowed_hosts:
97
+ return []
98
+ # ssrf_allowed_hosts is already a list[str], just clean and filter entries
99
+ return [host.strip() for host in allowed_hosts if host and host.strip()]
100
+
101
+
102
+ def is_host_allowed(hostname: str, ip: str | None = None) -> bool:
103
+ """Check if a hostname or IP is in the allowed hosts list.
104
+
105
+ Args:
106
+ hostname: Hostname to check
107
+ ip: Optional IP address to check
108
+
109
+ Returns:
110
+ bool: True if hostname or IP is in the allowed list, False otherwise.
111
+ """
112
+ allowed_hosts = get_allowed_hosts()
113
+ if not allowed_hosts:
114
+ return False
115
+
116
+ # Check hostname match
117
+ if hostname in allowed_hosts:
118
+ return True
119
+
120
+ # Check if hostname matches any wildcard patterns
121
+ for allowed in allowed_hosts:
122
+ if allowed.startswith("*."):
123
+ # Wildcard domain matching
124
+ domain_suffix = allowed[1:] # Remove the *
125
+ if hostname.endswith(domain_suffix) or hostname == domain_suffix[1:]:
126
+ return True
127
+
128
+ # Check IP-based matching if IP is provided
129
+ if ip:
130
+ try:
131
+ ip_obj = ipaddress.ip_address(ip)
132
+
133
+ # Check exact IP match
134
+ if ip in allowed_hosts:
135
+ return True
136
+
137
+ # Check CIDR range match
138
+ for allowed in allowed_hosts:
139
+ try:
140
+ # Try to parse as CIDR network
141
+ if "/" in allowed:
142
+ network = ipaddress.ip_network(allowed, strict=False)
143
+ if ip_obj in network:
144
+ return True
145
+ except (ValueError, ipaddress.AddressValueError):
146
+ # Not a valid CIDR, skip
147
+ continue
148
+
149
+ except (ValueError, ipaddress.AddressValueError):
150
+ # Invalid IP, skip IP-based checks
151
+ pass
152
+
153
+ return False
154
+
155
+
156
+ def is_ip_blocked(ip: str | ipaddress.IPv4Address | ipaddress.IPv6Address) -> bool:
157
+ """Check if an IP address is in a blocked range.
158
+
159
+ Args:
160
+ ip: IP address to check (string or ipaddress object)
161
+
162
+ Returns:
163
+ bool: True if IP is in a blocked range, False otherwise.
164
+ """
165
+ try:
166
+ ip_obj = ipaddress.ip_address(ip) if isinstance(ip, str) else ip
167
+
168
+ # Check against all blocked ranges
169
+ return any(ip_obj in blocked_range for blocked_range in get_blocked_ip_ranges())
170
+ except (ValueError, ipaddress.AddressValueError):
171
+ # If we can't parse the IP, treat it as blocked for safety
172
+ return True
173
+
174
+
175
+ def resolve_hostname(hostname: str) -> list[str]:
176
+ """Resolve a hostname to its IP addresses.
177
+
178
+ Args:
179
+ hostname: Hostname to resolve
180
+
181
+ Returns:
182
+ list[str]: List of resolved IP addresses
183
+
184
+ Raises:
185
+ SSRFProtectionError: If hostname cannot be resolved
186
+ """
187
+ try:
188
+ # Get address info for both IPv4 and IPv6
189
+ addr_info = socket.getaddrinfo(hostname, None)
190
+
191
+ # Extract unique IP addresses
192
+ ips = []
193
+ for info in addr_info:
194
+ ip = info[4][0]
195
+ # Remove IPv6 zone ID if present (e.g., "fe80::1%eth0" -> "fe80::1")
196
+ if "%" in ip:
197
+ ip = ip.split("%")[0]
198
+ if ip not in ips:
199
+ ips.append(ip)
200
+
201
+ if not ips:
202
+ msg = f"Unable to resolve hostname: {hostname}"
203
+ raise SSRFProtectionError(msg)
204
+ except socket.gaierror as e:
205
+ msg = f"DNS resolution failed for {hostname}: {e}"
206
+ raise SSRFProtectionError(msg) from e
207
+ except Exception as e:
208
+ msg = f"Error resolving hostname {hostname}: {e}"
209
+ raise SSRFProtectionError(msg) from e
210
+
211
+ return ips
212
+
213
+
214
+ def _validate_url_scheme(scheme: str) -> None:
215
+ """Validate that URL scheme is http or https.
216
+
217
+ Args:
218
+ scheme: URL scheme to validate
219
+
220
+ Raises:
221
+ SSRFProtectionError: If scheme is invalid
222
+ """
223
+ if scheme not in ("http", "https"):
224
+ msg = f"Invalid URL scheme '{scheme}'. Only http and https are allowed."
225
+ raise SSRFProtectionError(msg)
226
+
227
+
228
+ def _validate_hostname_exists(hostname: str | None) -> str:
229
+ """Validate that hostname exists in the URL.
230
+
231
+ Args:
232
+ hostname: Hostname to validate (may be None)
233
+
234
+ Returns:
235
+ str: The validated hostname
236
+
237
+ Raises:
238
+ SSRFProtectionError: If hostname is missing
239
+ """
240
+ if not hostname:
241
+ msg = "URL must contain a valid hostname"
242
+ raise SSRFProtectionError(msg)
243
+ return hostname
244
+
245
+
246
+ def _validate_direct_ip_address(hostname: str) -> bool:
247
+ """Validate a direct IP address in the URL.
248
+
249
+ Args:
250
+ hostname: Hostname that may be an IP address
251
+
252
+ Returns:
253
+ bool: True if hostname is a direct IP and validation passed,
254
+ False if hostname is not an IP (caller should continue with DNS resolution)
255
+
256
+ Raises:
257
+ SSRFProtectionError: If IP is blocked
258
+ """
259
+ try:
260
+ ip_obj = ipaddress.ip_address(hostname)
261
+ except ValueError:
262
+ # Not an IP address, it's a hostname - caller should continue with DNS resolution
263
+ return False
264
+
265
+ # It's a direct IP address
266
+ # Check if IP is in allowlist
267
+ if is_host_allowed(hostname, str(ip_obj)):
268
+ logger.debug("IP address %s is in allowlist, bypassing SSRF checks", hostname)
269
+ return True
270
+
271
+ if is_ip_blocked(ip_obj):
272
+ msg = (
273
+ f"Access to IP address {hostname} is blocked by SSRF protection. "
274
+ "Requests to private/internal IP ranges are not allowed for security reasons. "
275
+ "To allow this IP, add it to LANGFLOW_SSRF_ALLOWED_HOSTS environment variable."
276
+ )
277
+ raise SSRFProtectionError(msg)
278
+
279
+ # Direct IP is allowed (public IP)
280
+ return True
281
+
282
+
283
+ def _validate_hostname_resolution(hostname: str) -> None:
284
+ """Resolve hostname and validate resolved IPs are not blocked.
285
+
286
+ Args:
287
+ hostname: Hostname to resolve and validate
288
+
289
+ Raises:
290
+ SSRFProtectionError: If resolved IPs are blocked
291
+ """
292
+ # Resolve hostname to IP addresses
293
+ try:
294
+ resolved_ips = resolve_hostname(hostname)
295
+ except SSRFProtectionError:
296
+ # Re-raise SSRF errors as-is
297
+ raise
298
+ except Exception as e:
299
+ msg = f"Failed to resolve hostname {hostname}: {e}"
300
+ raise SSRFProtectionError(msg) from e
301
+
302
+ # Check if any resolved IP is blocked
303
+ blocked_ips = []
304
+ for ip in resolved_ips:
305
+ # Check if this specific IP is in the allowlist
306
+ if is_host_allowed(hostname, ip):
307
+ logger.debug("Resolved IP %s for hostname %s is in allowlist, bypassing SSRF checks", ip, hostname)
308
+ return
309
+
310
+ if is_ip_blocked(ip):
311
+ blocked_ips.append(ip)
312
+
313
+ if blocked_ips:
314
+ msg = (
315
+ f"Hostname {hostname} resolves to blocked IP address(es): {', '.join(blocked_ips)}. "
316
+ "Requests to private/internal IP ranges are not allowed for security reasons. "
317
+ "This protection prevents access to internal services, cloud metadata endpoints "
318
+ "(e.g., AWS 169.254.169.254), and other sensitive internal resources. "
319
+ "To allow this hostname, add it to LANGFLOW_SSRF_ALLOWED_HOSTS environment variable."
320
+ )
321
+ raise SSRFProtectionError(msg)
322
+
323
+
324
+ def validate_url_for_ssrf(url: str, *, warn_only: bool = True) -> None:
325
+ """Validate a URL to prevent SSRF attacks.
326
+
327
+ This function performs the following checks:
328
+ 1. Validates the URL scheme (only http/https allowed)
329
+ 2. Validates hostname exists
330
+ 3. Checks if hostname/IP is in allowlist
331
+ 4. If direct IP: validates it's not in blocked ranges
332
+ 5. If hostname: resolves to IPs and validates they're not in blocked ranges
333
+
334
+ Args:
335
+ url: URL to validate
336
+ warn_only: If True, only log warnings instead of raising errors (default: True)
337
+ TODO: Change default to False in next major version (2.0)
338
+
339
+ Raises:
340
+ SSRFProtectionError: If the URL is blocked due to SSRF protection (only if warn_only=False)
341
+ ValueError: If the URL is malformed
342
+ """
343
+ # Skip validation if SSRF protection is disabled
344
+ if not is_ssrf_protection_enabled():
345
+ return
346
+
347
+ # Parse URL
348
+ try:
349
+ parsed = urlparse(url)
350
+ except Exception as e:
351
+ msg = f"Invalid URL format: {e}"
352
+ raise ValueError(msg) from e
353
+
354
+ try:
355
+ # Validate scheme
356
+ _validate_url_scheme(parsed.scheme)
357
+ if parsed.scheme not in ("http", "https"):
358
+ return
359
+
360
+ # Validate hostname exists
361
+ hostname = _validate_hostname_exists(parsed.hostname)
362
+
363
+ # Check if hostname/IP is in allowlist (early return if allowed)
364
+ if is_host_allowed(hostname):
365
+ logger.debug("Hostname %s is in allowlist, bypassing SSRF checks", hostname)
366
+ return
367
+
368
+ # Validate direct IP address or resolve hostname
369
+ is_direct_ip = _validate_direct_ip_address(hostname)
370
+ if is_direct_ip:
371
+ # Direct IP was handled (allowed or exception raised)
372
+ return
373
+
374
+ # Not a direct IP, resolve hostname and validate
375
+ _validate_hostname_resolution(hostname)
376
+ except SSRFProtectionError as e:
377
+ if warn_only:
378
+ logger.warning("SSRF Protection Warning: %s [URL: %s]", str(e), url)
379
+ logger.warning(
380
+ "This request will be blocked when SSRF protection is enforced in the next major version. "
381
+ "Please review your API Request components."
382
+ )
383
+ return
384
+ raise
@@ -0,0 +1,26 @@
1
+ """Cloud environment validation utilities.
2
+
3
+ This module contains validation functions for cloud-specific constraints,
4
+ such as disabling certain features when running in Astra cloud environment.
5
+ """
6
+
7
+ import os
8
+
9
+
10
+ def raise_error_if_astra_cloud_disable_component(msg: str):
11
+ """Validate that we're not in an Astra cloud environment and certain components/features need to be disabled.
12
+
13
+ Check if the environment variable ASTRA_CLOUD_DISABLE_COMPONENT is set to true.
14
+ IF it is, then we know we are in an Astra cloud environment and
15
+ that certain components or component-features need to be disabled.
16
+
17
+ Args:
18
+ msg: The error message to raise if we're in an Astra cloud environment.
19
+
20
+ Raises:
21
+ ValueError: If running in an Astra cloud environment.
22
+ """
23
+ if (
24
+ disable_component := os.getenv("ASTRA_CLOUD_DISABLE_COMPONENT", "false")
25
+ ) and disable_component.lower().strip() == "true":
26
+ raise ValueError(msg)
@@ -1,18 +1,19 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lfx-nightly
3
- Version: 0.2.0.dev0
3
+ Version: 0.2.0.dev26
4
4
  Summary: Langflow Executor - A lightweight CLI tool for executing and serving Langflow AI flows
5
5
  Author-email: Gabriel Luiz Freitas Almeida <gabriel@langflow.org>
6
6
  Requires-Python: <3.14,>=3.10
7
7
  Requires-Dist: aiofile<4.0.0,>=3.8.0
8
8
  Requires-Dist: aiofiles<25.0.0,>=24.1.0
9
9
  Requires-Dist: asyncer<1.0.0,>=0.0.8
10
- Requires-Dist: cachetools<6.0.0,>=5.5.2
10
+ Requires-Dist: cachetools>=6.0.0
11
11
  Requires-Dist: chardet<6.0.0,>=5.2.0
12
12
  Requires-Dist: defusedxml<1.0.0,>=0.7.1
13
13
  Requires-Dist: docstring-parser<1.0.0,>=0.16
14
14
  Requires-Dist: emoji<3.0.0,>=2.14.1
15
15
  Requires-Dist: fastapi<1.0.0,>=0.115.13
16
+ Requires-Dist: filelock>=3.20.0
16
17
  Requires-Dist: httpx[http2]<1.0.0,>=0.24.0
17
18
  Requires-Dist: json-repair<1.0.0,>=0.30.3
18
19
  Requires-Dist: langchain-core<1.0.0,>=0.3.66
@@ -27,6 +28,7 @@ Requires-Dist: pillow<13.0.0,>=10.0.0
27
28
  Requires-Dist: platformdirs<5.0.0,>=4.3.8
28
29
  Requires-Dist: pydantic-settings<3.0.0,>=2.10.1
29
30
  Requires-Dist: pydantic<3.0.0,>=2.0.0
31
+ Requires-Dist: pypdf>=5.1.0
30
32
  Requires-Dist: python-dotenv<2.0.0,>=1.0.0
31
33
  Requires-Dist: rich<14.0.0,>=13.0.0
32
34
  Requires-Dist: structlog<26.0.0,>=25.4.0
@@ -198,6 +200,7 @@ Features:
198
200
  - Creates an agent with OpenAI GPT model
199
201
  - Provides web search tools via URLComponent
200
202
  - Connects ChatInput → Agent → ChatOutput
203
+ - Uses async get_graph() function for proper async handling
201
204
 
202
205
  Usage:
203
206
  uv run lfx run simple_agent.py "How are you?"
@@ -211,27 +214,40 @@ from lfx import components as cp
211
214
  from lfx.graph import Graph
212
215
  from lfx.log.logger import LogConfig
213
216
 
214
- log_config = LogConfig(
215
- log_level="INFO",
216
- log_file=Path("langflow.log"),
217
- )
218
217
 
219
- # Showcase the new flattened component access - no need for deep imports!
220
- chat_input = cp.ChatInput()
221
- agent = cp.AgentComponent()
222
- url_component = cp.URLComponent()
223
- tools = url_component.to_toolkit()
224
-
225
- agent.set(
226
- model_name="gpt-4.1-mini",
227
- agent_llm="OpenAI",
228
- api_key=os.getenv("OPENAI_API_KEY"),
229
- input_value=chat_input.message_response,
230
- tools=tools,
231
- )
232
- chat_output = cp.ChatOutput().set(input_value=agent.message_response)
233
-
234
- graph = Graph(chat_input, chat_output, log_config=log_config)
218
+ async def get_graph() -> Graph:
219
+ """Create and return the graph with async component initialization.
220
+
221
+ This function properly handles async component initialization without
222
+ blocking the module loading process. The script loader will detect this
223
+ async function and handle it appropriately.
224
+
225
+ Returns:
226
+ Graph: The configured graph with ChatInput → Agent → ChatOutput flow
227
+ """
228
+ log_config = LogConfig(
229
+ log_level="INFO",
230
+ log_file=Path("langflow.log"),
231
+ )
232
+
233
+ # Showcase the new flattened component access - no need for deep imports!
234
+ chat_input = cp.ChatInput()
235
+ agent = cp.AgentComponent()
236
+
237
+ # Use URLComponent for web search capabilities
238
+ url_component = cp.URLComponent()
239
+ tools = await url_component.to_toolkit()
240
+
241
+ agent.set(
242
+ model_name="gpt-4.1-mini",
243
+ agent_llm="OpenAI",
244
+ api_key=os.getenv("OPENAI_API_KEY"),
245
+ input_value=chat_input.message_response,
246
+ tools=tools,
247
+ )
248
+ chat_output = cp.ChatOutput().set(input_value=agent.message_response)
249
+
250
+ return Graph(chat_input, chat_output, log_config=log_config)
235
251
  ```
236
252
 
237
253
  **Step 2: Install dependencies**