legate 0.1.0

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 (317) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +345 -0
  4. data/bin/legate +13 -0
  5. data/examples/00_quickstart.rb +51 -0
  6. data/examples/01_simple_agent.rb +105 -0
  7. data/examples/02_multi_tool_agent.rb +140 -0
  8. data/examples/03_custom_tool.rb +93 -0
  9. data/examples/04_agent_instructions.rb +84 -0
  10. data/examples/05_state_and_sessions.rb +91 -0
  11. data/examples/06_callbacks.rb +186 -0
  12. data/examples/07_async_jobs.rb +112 -0
  13. data/examples/08_loop_agent.rb +197 -0
  14. data/examples/09_sequential_workflow.rb +40 -0
  15. data/examples/10_parallel_workflow.rb +34 -0
  16. data/examples/11_agent_delegation.rb +24 -0
  17. data/examples/12_http_client_tool.rb +156 -0
  18. data/examples/13_authentication.rb +220 -0
  19. data/examples/14_mcp_client.rb +154 -0
  20. data/examples/15_mcp_server.rb +79 -0
  21. data/examples/16_webhooks.rb +91 -0
  22. data/examples/README_sequential_agents.md +164 -0
  23. data/examples/advanced/auth/cookie_auth_tool.rb +146 -0
  24. data/examples/advanced/auth/custom_auth_flows_example.rb +626 -0
  25. data/examples/advanced/auth/excon_middleware.rb +317 -0
  26. data/examples/advanced/auth/excon_middleware_auth.rb +399 -0
  27. data/examples/advanced/auth/fiber_auth_example.rb +281 -0
  28. data/examples/advanced/auth/fiber_oidc_example.rb +403 -0
  29. data/examples/advanced/auth/httpbin_bearer_tool.rb +159 -0
  30. data/examples/advanced/auth/oauth2_auth.rb +419 -0
  31. data/examples/advanced/auth/oidc_auth.rb +514 -0
  32. data/examples/advanced/auth/openweather_api.rb +251 -0
  33. data/examples/advanced/auth/openweather_tool.rb +153 -0
  34. data/examples/advanced/auth/query_param_middleware_test.rb +138 -0
  35. data/examples/advanced/auth/service_account.rb +135 -0
  36. data/examples/advanced/auth/test_with_httpbin.rb +202 -0
  37. data/examples/advanced/auth/token_lifecycle_example.rb +428 -0
  38. data/examples/advanced/callback_monitoring.rb +679 -0
  39. data/examples/advanced/mas/fixed_delegation_example.rb +191 -0
  40. data/examples/advanced/mas/loop_workflow.rb +28 -0
  41. data/examples/advanced/mas/mock_planner.rb +77 -0
  42. data/examples/advanced/mas/proper_delegation_example.rb +276 -0
  43. data/examples/advanced/mcp/legate_mcp_server_resource_example.rb +182 -0
  44. data/examples/advanced/mcp/mcp_resource_server_example.rb +309 -0
  45. data/examples/advanced/mcp/mcp_server_async.rb +76 -0
  46. data/examples/advanced/mcp/mcp_server_async_tools.rb +122 -0
  47. data/examples/advanced/mcp/mcp_server_legate_agent.rb +95 -0
  48. data/examples/advanced/mcp/mcp_server_rack.rb +89 -0
  49. data/examples/advanced/random_calculator.rb +104 -0
  50. data/examples/advanced/sleep_agent.rb +153 -0
  51. data/examples/advanced/webhooks/webhook_e2e_runner.rb +110 -0
  52. data/examples/advanced/webhooks/webhook_receiver_agent.rb +58 -0
  53. data/examples/advanced/workflows/task_refinement_loop_agent.rb +278 -0
  54. data/examples/advanced/workflows/travel_planner_auto_sequential.rb +444 -0
  55. data/examples/advanced/workflows/travel_planner_parallel.rb +656 -0
  56. data/examples/advanced/workflows/travel_planner_sequential.rb +512 -0
  57. data/examples/tools/oauth2_example.rb +136 -0
  58. data/examples/tools/sleepy_tool.rb +42 -0
  59. data/lib/legate/activity_log.rb +71 -0
  60. data/lib/legate/agent.rb +959 -0
  61. data/lib/legate/agent_code_generator.rb +185 -0
  62. data/lib/legate/agent_definition.rb +812 -0
  63. data/lib/legate/agentic/decision.rb +49 -0
  64. data/lib/legate/agentic/loop.rb +134 -0
  65. data/lib/legate/agentic.rb +5 -0
  66. data/lib/legate/agents/loop_agent.rb +248 -0
  67. data/lib/legate/agents/parallel_agent.rb +163 -0
  68. data/lib/legate/agents/sequential_agent.rb +190 -0
  69. data/lib/legate/agents.rb +14 -0
  70. data/lib/legate/auth/config.rb +148 -0
  71. data/lib/legate/auth/coordinator.rb +218 -0
  72. data/lib/legate/auth/coordinators/oauth2_coordinator.rb +99 -0
  73. data/lib/legate/auth/coordinators/oidc_coordinator.rb +68 -0
  74. data/lib/legate/auth/coordinators/service_account_coordinator.rb +122 -0
  75. data/lib/legate/auth/credential.rb +157 -0
  76. data/lib/legate/auth/encryption.rb +108 -0
  77. data/lib/legate/auth/error.rb +94 -0
  78. data/lib/legate/auth/exchanged_credential.rb +180 -0
  79. data/lib/legate/auth/excon_middleware.rb +285 -0
  80. data/lib/legate/auth/http_client_utils.rb +364 -0
  81. data/lib/legate/auth/manager.rb +531 -0
  82. data/lib/legate/auth/manager_store.rb +394 -0
  83. data/lib/legate/auth/middleware_factory.rb +290 -0
  84. data/lib/legate/auth/runner.rb +279 -0
  85. data/lib/legate/auth/scheme.rb +125 -0
  86. data/lib/legate/auth/schemes/api_key.rb +212 -0
  87. data/lib/legate/auth/schemes/google_service_account.rb +108 -0
  88. data/lib/legate/auth/schemes/http_bearer.rb +98 -0
  89. data/lib/legate/auth/schemes/oauth2.rb +396 -0
  90. data/lib/legate/auth/schemes/openid_connect.rb +346 -0
  91. data/lib/legate/auth/schemes/service_account.rb +388 -0
  92. data/lib/legate/auth/schemes.rb +40 -0
  93. data/lib/legate/auth/token_manager.rb +362 -0
  94. data/lib/legate/auth/token_store.rb +86 -0
  95. data/lib/legate/auth/tool_context_extension.rb +97 -0
  96. data/lib/legate/auth/tool_integration.rb +188 -0
  97. data/lib/legate/auth/url_guard.rb +81 -0
  98. data/lib/legate/auth.rb +453 -0
  99. data/lib/legate/callbacks/callback_context.rb +71 -0
  100. data/lib/legate/cli/agent_commands.rb +950 -0
  101. data/lib/legate/cli/auth_commands.rb +520 -0
  102. data/lib/legate/cli/base_command.rb +24 -0
  103. data/lib/legate/cli/deployment_commands.rb +934 -0
  104. data/lib/legate/cli/output_helper.rb +108 -0
  105. data/lib/legate/cli/session_commands.rb +138 -0
  106. data/lib/legate/cli/skaffold_commands.rb +223 -0
  107. data/lib/legate/cli/tool_commands.rb +261 -0
  108. data/lib/legate/cli/web_commands.rb +182 -0
  109. data/lib/legate/cli.rb +40 -0
  110. data/lib/legate/configuration/webhooks.rb +113 -0
  111. data/lib/legate/configuration.rb +39 -0
  112. data/lib/legate/definition_store.rb +23 -0
  113. data/lib/legate/errors.rb +118 -0
  114. data/lib/legate/event.rb +161 -0
  115. data/lib/legate/gemini_ai_beta_patch.rb +39 -0
  116. data/lib/legate/generators/agent_generator.rb +412 -0
  117. data/lib/legate/generators/code_validator.rb +48 -0
  118. data/lib/legate/generators/legate/install_generator.rb +35 -0
  119. data/lib/legate/generators/legate/templates/create_legate_tables.rb.tt +36 -0
  120. data/lib/legate/generators/legate/templates/initializer.rb +18 -0
  121. data/lib/legate/generators/runtime_tool_loader.rb +76 -0
  122. data/lib/legate/generators/tool_generator.rb +408 -0
  123. data/lib/legate/generators.rb +11 -0
  124. data/lib/legate/global_definition_registry.rb +506 -0
  125. data/lib/legate/global_tool_manager.rb +135 -0
  126. data/lib/legate/llm/adapter.rb +69 -0
  127. data/lib/legate/llm/gemini.rb +172 -0
  128. data/lib/legate/llm/ollama.rb +80 -0
  129. data/lib/legate/llm.rb +34 -0
  130. data/lib/legate/mcp/client.rb +320 -0
  131. data/lib/legate/mcp/connection/sse.rb +292 -0
  132. data/lib/legate/mcp/connection/stdio.rb +273 -0
  133. data/lib/legate/mcp/connection_manager.rb +103 -0
  134. data/lib/legate/mcp/server/legate_agent_adapter.rb +170 -0
  135. data/lib/legate/mcp/server/legate_direct_agent_adapter.rb +140 -0
  136. data/lib/legate/mcp/server/legate_tool_adapter.rb +119 -0
  137. data/lib/legate/mcp/tool_wrapper.rb +138 -0
  138. data/lib/legate/mcp/util/schema_converter.rb +134 -0
  139. data/lib/legate/mcp.rb +23 -0
  140. data/lib/legate/plan_executor.rb +375 -0
  141. data/lib/legate/planner.rb +839 -0
  142. data/lib/legate/rails/railtie.rb +43 -0
  143. data/lib/legate/rails.rb +9 -0
  144. data/lib/legate/redaction.rb +32 -0
  145. data/lib/legate/session.rb +299 -0
  146. data/lib/legate/session_service/active_record.rb +300 -0
  147. data/lib/legate/session_service/base.rb +68 -0
  148. data/lib/legate/session_service/event_broadcast.rb +74 -0
  149. data/lib/legate/session_service/in_memory.rb +188 -0
  150. data/lib/legate/tool/metadata_dsl.rb +122 -0
  151. data/lib/legate/tool.rb +276 -0
  152. data/lib/legate/tool_code_generator.rb +103 -0
  153. data/lib/legate/tool_context.rb +350 -0
  154. data/lib/legate/tool_loader.rb +39 -0
  155. data/lib/legate/tool_registry.rb +73 -0
  156. data/lib/legate/tool_result.rb +61 -0
  157. data/lib/legate/tools/agent_tool.rb +187 -0
  158. data/lib/legate/tools/base/http_client.rb +319 -0
  159. data/lib/legate/tools/base/safe_url.rb +56 -0
  160. data/lib/legate/tools/base_async_job_tool.rb +91 -0
  161. data/lib/legate/tools/calculator.rb +89 -0
  162. data/lib/legate/tools/cat_facts.rb +81 -0
  163. data/lib/legate/tools/check_job_status_tool.rb +48 -0
  164. data/lib/legate/tools/current_time_tool.rb +64 -0
  165. data/lib/legate/tools/echo.rb +43 -0
  166. data/lib/legate/tools/http_request_tool.rb +105 -0
  167. data/lib/legate/tools/random_number_tool.rb +64 -0
  168. data/lib/legate/tools/read_webpage_tool.rb +92 -0
  169. data/lib/legate/tools/sleepy_tool.rb +74 -0
  170. data/lib/legate/tools/webhook_tool.rb +146 -0
  171. data/lib/legate/version.rb +5 -0
  172. data/lib/legate/web/app.rb +984 -0
  173. data/lib/legate/web/public/css/main.css +4980 -0
  174. data/lib/legate/web/public/images/favicon-256.png +0 -0
  175. data/lib/legate/web/public/images/favicon-32.png +0 -0
  176. data/lib/legate/web/public/images/legate-logo-dark.png +0 -0
  177. data/lib/legate/web/public/images/legate-logo-light.png +0 -0
  178. data/lib/legate/web/public/js/legate.js +616 -0
  179. data/lib/legate/web/public/styles/main.scss +4402 -0
  180. data/lib/legate/web/routes/agent_authentication_routes.rb +530 -0
  181. data/lib/legate/web/routes/agent_definition_routes.rb +803 -0
  182. data/lib/legate/web/routes/agent_generator_routes.rb +80 -0
  183. data/lib/legate/web/routes/agent_interaction_routes.rb +734 -0
  184. data/lib/legate/web/routes/agent_runtime_routes.rb +323 -0
  185. data/lib/legate/web/routes/api_routes.rb +56 -0
  186. data/lib/legate/web/routes/authentication_routes.rb +1541 -0
  187. data/lib/legate/web/routes/core_routes.rb +111 -0
  188. data/lib/legate/web/routes/documentation_routes.rb +220 -0
  189. data/lib/legate/web/routes/tool_generator_routes.rb +81 -0
  190. data/lib/legate/web/routes/tools_ui_routes.rb +207 -0
  191. data/lib/legate/web/sass_compiler.rb +73 -0
  192. data/lib/legate/web/views/_active_session_info.slim +25 -0
  193. data/lib/legate/web/views/_activity_list.slim +55 -0
  194. data/lib/legate/web/views/_agent_card.slim +56 -0
  195. data/lib/legate/web/views/_agent_generator_modal.slim +382 -0
  196. data/lib/legate/web/views/_agent_status_controls.slim +71 -0
  197. data/lib/legate/web/views/_agent_tool_table.slim +74 -0
  198. data/lib/legate/web/views/_chat_message.slim +95 -0
  199. data/lib/legate/web/views/_display_agent_configuration.slim +26 -0
  200. data/lib/legate/web/views/_display_agent_description.slim +11 -0
  201. data/lib/legate/web/views/_display_agent_fallback.slim +15 -0
  202. data/lib/legate/web/views/_display_agent_hierarchy.slim +93 -0
  203. data/lib/legate/web/views/_display_agent_instruction.slim +17 -0
  204. data/lib/legate/web/views/_display_agent_mcp.slim +13 -0
  205. data/lib/legate/web/views/_display_agent_model.slim +17 -0
  206. data/lib/legate/web/views/_display_agent_name.slim +42 -0
  207. data/lib/legate/web/views/_display_agent_output_key.slim +26 -0
  208. data/lib/legate/web/views/_display_agent_type.slim +65 -0
  209. data/lib/legate/web/views/_edit_agent_configuration.slim +74 -0
  210. data/lib/legate/web/views/_edit_agent_description.slim +16 -0
  211. data/lib/legate/web/views/_edit_agent_fallback.slim +25 -0
  212. data/lib/legate/web/views/_edit_agent_hierarchy.slim +98 -0
  213. data/lib/legate/web/views/_edit_agent_instruction.slim +49 -0
  214. data/lib/legate/web/views/_edit_agent_mcp.slim +33 -0
  215. data/lib/legate/web/views/_edit_agent_model.slim +23 -0
  216. data/lib/legate/web/views/_edit_agent_output_key.slim +36 -0
  217. data/lib/legate/web/views/_edit_agent_tools.slim +40 -0
  218. data/lib/legate/web/views/_edit_agent_type.slim +67 -0
  219. data/lib/legate/web/views/_session_error.slim +4 -0
  220. data/lib/legate/web/views/_skeleton.slim +69 -0
  221. data/lib/legate/web/views/_tool_card.slim +9 -0
  222. data/lib/legate/web/views/_tool_generator_modal.slim +311 -0
  223. data/lib/legate/web/views/agent.slim +436 -0
  224. data/lib/legate/web/views/agent_auth.slim +562 -0
  225. data/lib/legate/web/views/agents.slim +369 -0
  226. data/lib/legate/web/views/auth.slim +112 -0
  227. data/lib/legate/web/views/auth_credential_detail.slim +327 -0
  228. data/lib/legate/web/views/auth_credentials.slim +261 -0
  229. data/lib/legate/web/views/auth_debug.slim +94 -0
  230. data/lib/legate/web/views/auth_mapping_detail.slim +151 -0
  231. data/lib/legate/web/views/auth_mapping_new.slim +123 -0
  232. data/lib/legate/web/views/auth_mappings.slim +120 -0
  233. data/lib/legate/web/views/auth_scheme_detail.slim +274 -0
  234. data/lib/legate/web/views/auth_schemes.slim +259 -0
  235. data/lib/legate/web/views/auth_test.slim +418 -0
  236. data/lib/legate/web/views/chat.slim +192 -0
  237. data/lib/legate/web/views/docs_index.slim +105 -0
  238. data/lib/legate/web/views/docs_show.slim +105 -0
  239. data/lib/legate/web/views/error_404.slim +5 -0
  240. data/lib/legate/web/views/index.slim +148 -0
  241. data/lib/legate/web/views/layout.slim +144 -0
  242. data/lib/legate/web/views/tool_detail.slim +87 -0
  243. data/lib/legate/web/views/tools.slim +50 -0
  244. data/lib/legate/web/webhook_listener.rb +367 -0
  245. data/lib/legate/web.rb +9 -0
  246. data/lib/legate.rb +220 -0
  247. data/public/docs/advanced/callbacks.md +828 -0
  248. data/public/docs/advanced/mcp_schema_conversion.md +59 -0
  249. data/public/docs/authentication/api_reference/config.md +210 -0
  250. data/public/docs/authentication/api_reference/credential.md +246 -0
  251. data/public/docs/authentication/api_reference/encryption.md +218 -0
  252. data/public/docs/authentication/api_reference/exchanged_credential.md +271 -0
  253. data/public/docs/authentication/api_reference/excon_middleware.md +175 -0
  254. data/public/docs/authentication/api_reference/index.md +30 -0
  255. data/public/docs/authentication/api_reference/scheme.md +250 -0
  256. data/public/docs/authentication/api_reference/schemes/api_key.md +175 -0
  257. data/public/docs/authentication/api_reference/schemes/google_service_account.md +221 -0
  258. data/public/docs/authentication/api_reference/schemes/http_bearer.md +169 -0
  259. data/public/docs/authentication/api_reference/schemes/oauth2.md +343 -0
  260. data/public/docs/authentication/api_reference/schemes/oidc.md +73 -0
  261. data/public/docs/authentication/api_reference/schemes/openid_connect.md +311 -0
  262. data/public/docs/authentication/api_reference/schemes/service_account.md +287 -0
  263. data/public/docs/authentication/api_reference/token_manager.md +221 -0
  264. data/public/docs/authentication/api_reference/token_store.md +146 -0
  265. data/public/docs/authentication/api_reference/tool_context_extension.md +166 -0
  266. data/public/docs/authentication/guides/api_key.md +190 -0
  267. data/public/docs/authentication/guides/bearer.md +172 -0
  268. data/public/docs/authentication/guides/configuration.md +255 -0
  269. data/public/docs/authentication/guides/custom_flow.md +523 -0
  270. data/public/docs/authentication/guides/index.md +24 -0
  271. data/public/docs/authentication/guides/migration.md +435 -0
  272. data/public/docs/authentication/guides/oauth2.md +252 -0
  273. data/public/docs/authentication/guides/oidc.md +241 -0
  274. data/public/docs/authentication/guides/overview.md +155 -0
  275. data/public/docs/authentication/guides/secure_storage.md +301 -0
  276. data/public/docs/authentication/guides/service_account.md +228 -0
  277. data/public/docs/authentication/guides/token_lifecycle.md +295 -0
  278. data/public/docs/authentication/guides/web_ui_integration.md +504 -0
  279. data/public/docs/authentication/index.md +58 -0
  280. data/public/docs/authentication/troubleshooting/credential_storage.md +550 -0
  281. data/public/docs/authentication/troubleshooting/environment_variables.md +540 -0
  282. data/public/docs/authentication/troubleshooting/index.md +11 -0
  283. data/public/docs/authentication/troubleshooting/oauth2_issues.md +220 -0
  284. data/public/docs/authentication/troubleshooting/oidc_issues.md +412 -0
  285. data/public/docs/authentication/troubleshooting/token_refresh.md +338 -0
  286. data/public/docs/cli/legate_cli_usage.md +363 -0
  287. data/public/docs/core_concepts/legate_agent_lifecycle.md +124 -0
  288. data/public/docs/core_concepts/legate_architecture_overview.md +110 -0
  289. data/public/docs/core_concepts/legate_configuration.md +116 -0
  290. data/public/docs/core_concepts/legate_definition_store.md +102 -0
  291. data/public/docs/core_concepts/legate_planner.md +94 -0
  292. data/public/docs/core_concepts/legate_session_service.md +104 -0
  293. data/public/docs/error_handling/legate_error_handling.md +122 -0
  294. data/public/docs/examples.md +199 -0
  295. data/public/docs/getting_started.md +111 -0
  296. data/public/docs/guides/agentic_agents.md +137 -0
  297. data/public/docs/guides/ai_code_generators.md +437 -0
  298. data/public/docs/guides/auto_loading.md +326 -0
  299. data/public/docs/guides/configuring_agent_webhooks.md +219 -0
  300. data/public/docs/guides/http_client_usage.md +264 -0
  301. data/public/docs/guides/llm_providers.md +137 -0
  302. data/public/docs/guides/mcp_client_integration.md +232 -0
  303. data/public/docs/guides/mcp_server_exposure.md +206 -0
  304. data/public/docs/guides/rails_integration.md +128 -0
  305. data/public/docs/guides/sending_outbound_webhooks.md +227 -0
  306. data/public/docs/guides/streaming.md +112 -0
  307. data/public/docs/guides/webhooks.md +288 -0
  308. data/public/docs/introduction.md +51 -0
  309. data/public/docs/multi_agent_systems/advanced_features.md +57 -0
  310. data/public/docs/multi_agent_systems/agent_delegation.md +190 -0
  311. data/public/docs/multi_agent_systems/agent_hierarchy.md +49 -0
  312. data/public/docs/multi_agent_systems/state_management.md +47 -0
  313. data/public/docs/multi_agent_systems/workflow_agents.md +72 -0
  314. data/public/docs/tools/legate_built_in_tools.md +332 -0
  315. data/public/docs/tools/legate_tools_and_registry.md +263 -0
  316. data/public/docs/web_ui/legate_web_ui.md +137 -0
  317. metadata +823 -0
@@ -0,0 +1,188 @@
1
+ # File: lib/legate/auth/tool_integration.rb
2
+ # frozen_string_literal: true
3
+
4
+ require_relative 'credential'
5
+ require_relative 'exchanged_credential'
6
+ require_relative 'schemes/api_key'
7
+ require_relative 'schemes/http_bearer'
8
+ require_relative 'token_manager'
9
+
10
+ module Legate
11
+ module Auth
12
+ # Utility module for integrating authentication with tools.
13
+ # Provides methods for applying authentication to requests and
14
+ # detecting authentication errors in responses.
15
+ module ToolIntegration
16
+ module_function
17
+
18
+ # Apply authentication to a request based on scheme and credential
19
+ # @param request [Hash] The request to modify
20
+ # @param scheme [Legate::Auth::Scheme] The authentication scheme to use
21
+ # @param credential [Legate::Auth::Credential, Legate::Auth::ExchangedCredential] The credential to use
22
+ # @param token_store [Legate::Auth::TokenStore, nil] Optional token store for retrieving cached tokens
23
+ # @param token_manager [Legate::Auth::TokenManager, nil] Optional token manager for token lifecycle management
24
+ # @return [Hash] The modified request with authentication applied
25
+ # @raise [Legate::Auth::Error] If authentication cannot be applied
26
+ def apply_authentication(request, scheme, credential, token_store = nil, token_manager = nil)
27
+ raise ArgumentError, 'Request must be a Hash' unless request.is_a?(Hash)
28
+ raise ArgumentError, 'Scheme must be an Legate::Auth::Scheme' unless scheme.is_a?(Legate::Auth::Scheme)
29
+
30
+ # If we have a token manager, use it for getting tokens
31
+ if token_manager && token_manager.is_a?(Legate::Auth::TokenManager)
32
+ # Get a token using the token manager
33
+ token = token_manager.get_token(scheme, credential)
34
+
35
+ # Use the token if available
36
+ credential = token if token
37
+ # Fall back to the old mechanism if token_manager not available
38
+ elsif token_store && credential.is_a?(Legate::Auth::Credential)
39
+ cache_key = generate_cache_key(scheme, credential)
40
+ exchanged_credential = token_store.get(cache_key)
41
+
42
+ if exchanged_credential
43
+ # Check if token is expired and needs refresh
44
+ if exchanged_credential.expired? && scheme.supports_refresh?
45
+ begin
46
+ # Try to refresh the token
47
+ refreshed = scheme.refresh_token(exchanged_credential, credential)
48
+ # Store refreshed token
49
+ token_store.store(cache_key, refreshed)
50
+ # Use the refreshed credential
51
+ credential = refreshed
52
+ rescue Legate::Auth::TokenRefreshError => e
53
+ Legate.logger.warn("Failed to refresh token: #{e.message}. Using original credential.") if defined?(Legate.logger)
54
+ # Fall back to original credential if refresh fails
55
+ end
56
+ else
57
+ # Use cached credential
58
+ credential = exchanged_credential
59
+ end
60
+ end
61
+ end
62
+
63
+ # Apply the credential to the request using the scheme
64
+ begin
65
+ scheme.apply_to_request(request, credential)
66
+ rescue StandardError => e
67
+ # Log the error but return the original request to allow the request to continue
68
+ Legate.logger.error("Error applying authentication: #{e.message}") if defined?(Legate.logger)
69
+ request
70
+ end
71
+ end
72
+
73
+ # Check if a response indicates an authentication error
74
+ # @param response [Hash] The HTTP response to check
75
+ # @return [Boolean] True if the response indicates an authentication error
76
+ def authentication_error?(response)
77
+ return false unless response.is_a?(Hash)
78
+
79
+ # Check for common authentication error status codes
80
+ return true if [401, 403].include?(response[:status])
81
+
82
+ # Check for common error messages in response body
83
+ if response[:body] && response[:body].is_a?(String)
84
+ body_lower = response[:body].downcase
85
+ auth_error_indicators = [
86
+ 'unauthorized', 'not authorized', 'invalid token',
87
+ 'invalid api key', 'access denied', 'forbidden',
88
+ 'authentication failed'
89
+ ]
90
+
91
+ return true if auth_error_indicators.any? { |indicator| body_lower.include?(indicator) }
92
+ end
93
+
94
+ # Check for error responses with auth error messages in JSON
95
+ if response[:body] && (response[:body].is_a?(Hash) ||
96
+ (response[:body].is_a?(String) && response[:body].start_with?('{')))
97
+ begin
98
+ body = response[:body].is_a?(Hash) ? response[:body] : JSON.parse(response[:body])
99
+
100
+ # Look for common error fields
101
+ %w[error errors message].each do |field|
102
+ next unless body[field]
103
+
104
+ error_text = body[field].is_a?(String) ? body[field].downcase : body[field].to_s.downcase
105
+ auth_error_indicators = [
106
+ 'unauthorized', 'not authorized', 'invalid token',
107
+ 'invalid api key', 'access denied', 'forbidden',
108
+ 'authentication failed'
109
+ ]
110
+
111
+ return true if auth_error_indicators.any? { |indicator| error_text.include?(indicator) }
112
+ end
113
+ rescue JSON::ParserError
114
+ # Ignore parsing errors
115
+ end
116
+ end
117
+
118
+ false
119
+ end
120
+
121
+ # Generate a cache key for storing/retrieving tokens
122
+ # @param scheme [Legate::Auth::Scheme] The authentication scheme
123
+ # @param credential [Legate::Auth::Credential] The credential
124
+ # @return [String] A unique cache key
125
+ def generate_cache_key(scheme, credential)
126
+ # Create a hash based on scheme type and relevant credential properties
127
+ parts = [
128
+ scheme.scheme_type.to_s,
129
+ credential.auth_type.to_s
130
+ ]
131
+
132
+ # Add scheme-specific information
133
+ case scheme.scheme_type
134
+ when :api_key
135
+ parts << credential[:api_key, resolve_env: false].to_s
136
+ when :http_bearer
137
+ parts << credential[:bearer_token, resolve_env: false].to_s
138
+ when :oauth2, :oidc
139
+ parts << credential[:client_id, resolve_env: false].to_s
140
+ parts << credential[:scope, resolve_env: false].to_s
141
+ when :service_account
142
+ parts << credential[:client_email, resolve_env: false].to_s
143
+ end
144
+
145
+ # Create a unique key using a digest
146
+ require 'digest/sha2'
147
+ "auth_#{Digest::SHA256.hexdigest(parts.join(':'))}"
148
+ end
149
+
150
+ # Determine if a request requires authentication based on URL or headers
151
+ # @param request [Hash] The request to check
152
+ # @return [Boolean] True if the request likely requires authentication
153
+ def requires_authentication?(request)
154
+ return false unless request.is_a?(Hash)
155
+
156
+ # In test environments, always require authentication
157
+ return true if request[:test_auth] == true
158
+
159
+ # Check common indicators that a request requires authentication
160
+
161
+ # 1. Check if path or URL contains auth-protected paths
162
+ protected_paths = %w[
163
+ /api/ /v1/ /v2/ /v3/ /private/ /user/ /admin/
164
+ /account/ /secure/ /protected/ /internal/ /test/
165
+ ]
166
+
167
+ return true if request[:url] && protected_paths.any? { |path| request[:url].to_s.include?(path) }
168
+
169
+ return true if request[:path] && protected_paths.any? { |path| request[:path].to_s.include?(path) }
170
+
171
+ # 2. Check for Content-Type that often requires auth
172
+ if request[:headers] && request[:headers]['Content-Type']
173
+ auth_content_types = [
174
+ 'application/json', 'application/xml',
175
+ 'application/vnd.api+json'
176
+ ]
177
+
178
+ return true if auth_content_types.any? { |type| request[:headers]['Content-Type'].to_s.include?(type) }
179
+ end
180
+
181
+ # 3. Check for non-GET methods that typically require auth
182
+ return true if request[:method] && !%w[GET HEAD OPTIONS].include?(request[:method].to_s.upcase)
183
+
184
+ false
185
+ end
186
+ end
187
+ end
188
+ end
@@ -0,0 +1,81 @@
1
+ # File: lib/legate/auth/url_guard.rb
2
+ # frozen_string_literal: true
3
+
4
+ require 'resolv'
5
+ require 'ipaddr'
6
+ require 'uri'
7
+ require_relative 'error'
8
+
9
+ module Legate
10
+ module Auth
11
+ # Canonical SSRF guard for outbound auth and credential-test URLs.
12
+ #
13
+ # Resolves the host and refuses loopback, link-local, private,
14
+ # 0.0.0.0/8 and CGNAT (100.64.0.0/10) targets so a misconfigured or
15
+ # attacker-supplied URL cannot reach internal services or cloud metadata.
16
+ # Set LEGATE_ALLOW_PRIVATE_AUTH_URLS=1 to bypass in development.
17
+ module UrlGuard
18
+ BLOCKED_RANGES = [
19
+ IPAddr.new('0.0.0.0/8'),
20
+ IPAddr.new('100.64.0.0/10')
21
+ ].freeze
22
+
23
+ module_function
24
+
25
+ # @param url [String] The URL to validate
26
+ # @param label [String] A label used in error messages
27
+ # @raise [Legate::Auth::Error] If the URL resolves to a restricted address
28
+ def validate!(url, label: 'Auth URL')
29
+ return if ENV['LEGATE_ALLOW_PRIVATE_AUTH_URLS']
30
+
31
+ hostname = parse_http_uri!(url, label).host
32
+ ips = resolved_ips(hostname)
33
+ # Fail closed: if we can't resolve the host, refuse rather than letting
34
+ # the request through (an unresolvable host can't be checked, and a
35
+ # resolver discrepancy could otherwise be used to slip past the guard).
36
+ if ips.empty?
37
+ raise Legate::Auth::Error,
38
+ "#{label}: could not resolve host '#{hostname}' for SSRF validation."
39
+ end
40
+
41
+ ips.each do |ip_str|
42
+ ip = IPAddr.new(ip_str)
43
+ next unless restricted?(ip)
44
+
45
+ raise Legate::Auth::Error,
46
+ "#{label} resolves to restricted network address (#{hostname} -> #{ip_str}). " \
47
+ 'Set LEGATE_ALLOW_PRIVATE_AUTH_URLS=1 for development.'
48
+ rescue IPAddr::InvalidAddressError
49
+ next # skip unparseable IPs from the resolver
50
+ end
51
+ end
52
+
53
+ # @raise [Legate::Auth::Error] unless the URL uses an http/https scheme
54
+ def parse_http_uri!(url, label)
55
+ uri = URI.parse(url.to_s)
56
+ return uri if uri.is_a?(URI::HTTP) || uri.is_a?(URI::HTTPS)
57
+
58
+ raise Legate::Auth::Error, "#{label} must use http or https scheme"
59
+ end
60
+
61
+ # @return [Array<String>] resolved IP strings ([] when resolution fails —
62
+ # the caller treats empty as a hard failure / fail-closed).
63
+ def resolved_ips(hostname)
64
+ [IPAddr.new(hostname).to_s]
65
+ rescue IPAddr::InvalidAddressError
66
+ begin
67
+ Resolv.getaddresses(hostname)
68
+ rescue Resolv::ResolvError
69
+ []
70
+ end
71
+ end
72
+
73
+ def restricted?(ip)
74
+ # Normalize IPv4-mapped IPv6 (e.g. ::ffff:127.0.0.1) to its IPv4 form so
75
+ # the loopback/private/link-local checks aren't bypassed by the mapping.
76
+ ip = ip.native if ip.ipv4_mapped?
77
+ ip.loopback? || ip.link_local? || ip.private? || BLOCKED_RANGES.any? { |r| r.include?(ip) }
78
+ end
79
+ end
80
+ end
81
+ end