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,279 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'error'
4
+ require_relative 'coordinator'
5
+ require_relative 'coordinators/oauth2_coordinator'
6
+ require_relative 'coordinators/oidc_coordinator'
7
+ require_relative 'coordinators/service_account_coordinator'
8
+ require_relative 'token_store'
9
+ require_relative 'token_manager'
10
+
11
+ module Legate
12
+ module Auth
13
+ # Runner provides the execution environment for fiber-based authentication flows.
14
+ # It handles creating and managing authentication coordinators, running tasks within
15
+ # a fiber, and handling authentication requests/responses.
16
+ class Runner
17
+ # Initialize a new authentication runner
18
+ # @param session_service [Legate::SessionService::Base] The session service for persistence
19
+ # @param token_store [Legate::Auth::TokenStore, nil] Optional token store for caching tokens
20
+ # @param token_manager [Legate::Auth::TokenManager, nil] Optional token manager for lifecycle management
21
+ def initialize(session_service:, token_store: nil, token_manager: nil)
22
+ @session_service = session_service
23
+ @token_store = token_store || TokenStore.new(session_service)
24
+ @token_manager = token_manager || TokenManager.new(@token_store)
25
+ @active_coordinators = {}
26
+ end
27
+
28
+ # Run a task within a fiber with authentication handling
29
+ # @param task [Proc] The task to run
30
+ # @param context [Object] The context to run the task in (typically ToolContext)
31
+ # @yield [Hash, nil] Optional block to handle authentication requests
32
+ # @return [Object] The result of the task
33
+ # @raise [Legate::Auth::Error] If authentication fails
34
+ def run(task, context = nil, &auth_handler)
35
+ raise ArgumentError, 'Task must be a Proc or lambda' unless task.is_a?(Proc)
36
+
37
+ # Create a fiber for the task
38
+ task_fiber = Fiber.new do
39
+ # Make the auth_session method available in the context
40
+ if context && !context.respond_to?(:auth_session)
41
+ context.define_singleton_method(:auth_session) do |scheme, credential, **opts|
42
+ Fiber.yield({
43
+ action: :authenticate,
44
+ scheme: scheme,
45
+ credential: credential,
46
+ options: opts
47
+ })
48
+ end
49
+ end
50
+
51
+ # Run the task
52
+ task.call
53
+ rescue StandardError => e
54
+ { error: e }
55
+ end
56
+
57
+ # Start the fiber
58
+ result = nil
59
+ loop do
60
+ # Resume the fiber and get the next result/yield
61
+ result = task_fiber.resume
62
+
63
+ # If the fiber has completed (not yielded), return the result
64
+ break unless task_fiber.alive?
65
+
66
+ # Handle authentication requests
67
+ if result.is_a?(Hash) && result[:action] == :authenticate
68
+ handle_authentication_request(result, task_fiber, &auth_handler)
69
+ else
70
+ # For other types of yields, just pass them to the handler if provided
71
+ response = auth_handler ? auth_handler.call(result) : nil
72
+ result = task_fiber.resume(response)
73
+ end
74
+ end
75
+
76
+ # If the result is an error hash, raise it
77
+ raise result[:error] if result.is_a?(Hash) && result[:error]
78
+
79
+ result
80
+ end
81
+
82
+ # Handle an authentication response from the client
83
+ # @param request_id [String] The request ID for the authentication flow
84
+ # @param response [Hash] The response from the client
85
+ # @return [Hash] The result of handling the response
86
+ def handle_auth_response(request_id, response)
87
+ coordinator = @active_coordinators[request_id]
88
+
89
+ unless coordinator
90
+ return {
91
+ status: :error,
92
+ error: "No active authentication flow found for request ID: #{request_id}"
93
+ }
94
+ end
95
+
96
+ begin
97
+ result = coordinator.resume(response)
98
+
99
+ if coordinator.complete?
100
+ if coordinator.success?
101
+ # Authentication completed successfully
102
+ @active_coordinators.delete(request_id)
103
+ {
104
+ status: :completed,
105
+ credential: result
106
+ }
107
+ else
108
+ # Authentication failed
109
+ @active_coordinators.delete(request_id)
110
+ {
111
+ status: :failed,
112
+ error: coordinator.error&.message || 'Authentication failed'
113
+ }
114
+ end
115
+ else
116
+ # Authentication is still in progress, return the next request
117
+ {
118
+ status: :pending,
119
+ request: result
120
+ }
121
+ end
122
+ rescue StandardError => e
123
+ @active_coordinators.delete(request_id)
124
+ {
125
+ status: :error,
126
+ error: "Error handling authentication response: #{e.message}"
127
+ }
128
+ end
129
+ end
130
+
131
+ # Cancel an active authentication flow
132
+ # @param request_id [String] The request ID for the authentication flow
133
+ # @param reason [String, nil] Optional reason for cancellation
134
+ # @return [Boolean] True if the flow was successfully cancelled
135
+ def cancel_auth_flow(request_id, reason = nil)
136
+ coordinator = @active_coordinators[request_id]
137
+
138
+ return false unless coordinator
139
+
140
+ result = coordinator.cancel(reason)
141
+ @active_coordinators.delete(request_id) if result
142
+ result
143
+ end
144
+
145
+ private
146
+
147
+ # Handle an authentication request yielded from a task fiber
148
+ # @param request [Hash] The authentication request
149
+ # @param task_fiber [Fiber] The task fiber
150
+ # @yield [Hash] Optional block to handle authentication requests
151
+ # @return [Legate::Auth::ExchangedCredential, nil] The result of authentication
152
+ def handle_authentication_request(request, task_fiber, &auth_handler)
153
+ scheme = request[:scheme]
154
+ credential = request[:credential]
155
+ options = request[:options] || {}
156
+
157
+ # Validate request
158
+ raise ArgumentError, "Invalid authentication scheme: #{scheme.class}" unless scheme.is_a?(Legate::Auth::Scheme)
159
+
160
+ raise ArgumentError, "Invalid credential: #{credential.class}" unless credential.is_a?(Legate::Auth::Credential)
161
+
162
+ # First, try to get an existing token from the token manager
163
+ token = @token_manager.get_token(scheme, credential)
164
+
165
+ # If we have a valid token, use it
166
+ return task_fiber.resume(token) if token && !token.expired?
167
+
168
+ # Create an appropriate coordinator based on the scheme type
169
+ coordinator = create_coordinator(scheme, credential, options)
170
+
171
+ # Start the authentication flow
172
+ auth_request = coordinator.start
173
+
174
+ # Store the coordinator for future responses
175
+ @active_coordinators[auth_request[:request_id]] = coordinator
176
+
177
+ # If a handler block is provided, use it
178
+ if auth_handler
179
+ # Pass the authentication request to the handler
180
+ response = auth_handler.call(auth_request)
181
+
182
+ # If the handler provided a response directly, process it
183
+ if response
184
+ result = handle_auth_response(auth_request[:request_id], response)
185
+
186
+ # Return the credential to the task fiber if authentication completed
187
+ return task_fiber.resume(result[:credential]) if result[:status] == :completed
188
+ end
189
+ end
190
+
191
+ # Otherwise, return authentication request to await client response
192
+ { request_id: auth_request[:request_id], status: :pending }
193
+ end
194
+
195
+ # Create an appropriate coordinator based on the scheme type
196
+ # @param scheme [Legate::Auth::Scheme] The authentication scheme
197
+ # @param credential [Legate::Auth::Credential] The credential
198
+ # @param options [Hash] Additional options for the coordinator
199
+ # @return [Legate::Auth::Coordinator] The appropriate coordinator
200
+ def create_coordinator(scheme, credential, options)
201
+ case scheme
202
+ when Legate::Auth::Schemes::OAuth2
203
+ Legate::Auth::Coordinators::OAuth2Coordinator.new(
204
+ scheme: scheme,
205
+ credential: credential,
206
+ session_service: @session_service,
207
+ token_store: @token_store,
208
+ timeout: options[:timeout],
209
+ redirect_uri: options[:redirect_uri]
210
+ )
211
+ when Legate::Auth::Schemes::OIDC
212
+ Legate::Auth::Coordinators::OIDCCoordinator.new(
213
+ scheme: scheme,
214
+ credential: credential,
215
+ session_service: @session_service,
216
+ token_store: @token_store,
217
+ timeout: options[:timeout],
218
+ redirect_uri: options[:redirect_uri]
219
+ )
220
+ when Legate::Auth::Schemes::ServiceAccount
221
+ Legate::Auth::Coordinators::ServiceAccountCoordinator.new(
222
+ scheme: scheme,
223
+ credential: credential,
224
+ session_service: @session_service,
225
+ token_store: @token_store,
226
+ timeout: options[:timeout]
227
+ )
228
+ # Add more coordinator types as needed for other schemes
229
+ else
230
+ raise NotImplementedError, "No coordinator available for scheme type: #{scheme.class}"
231
+ end
232
+ end
233
+
234
+ # Create a service account coordinator
235
+ # @param scheme [Legate::Auth::Schemes::ServiceAccount] The service account scheme
236
+ # @param credential [Legate::Auth::Credential] The credential with service account info
237
+ # @param options [Hash] Additional options for the coordinator
238
+ # @return [Legate::Auth::Coordinators::ServiceAccountCoordinator] The coordinator
239
+ def create_service_account_coordinator(scheme, credential, options = {})
240
+ raise ArgumentError, "Expected a ServiceAccount scheme, got #{scheme.class}" unless scheme.is_a?(Legate::Auth::Schemes::ServiceAccount)
241
+
242
+ raise ArgumentError, "Credential must have auth_type :service_account, got #{credential.auth_type}" unless credential.auth_type.to_sym == :service_account
243
+
244
+ Legate::Auth::Coordinators::ServiceAccountCoordinator.new(
245
+ scheme: scheme,
246
+ credential: credential,
247
+ session_service: @session_service,
248
+ token_store: @token_store,
249
+ timeout: options[:timeout]
250
+ )
251
+ end
252
+
253
+ # Authenticate using a service account
254
+ # @param scheme [Legate::Auth::Schemes::ServiceAccount] The service account scheme
255
+ # @param credential [Legate::Auth::Credential] The credential with service account info
256
+ # @param options [Hash] Additional options for the coordinator
257
+ # @return [Legate::Auth::ExchangedCredential] The authenticated credential
258
+ # @raise [Legate::Auth::Error] If authentication fails
259
+ def authenticate_with_service_account(scheme, credential, options = {})
260
+ # Create the coordinator
261
+ coordinator = create_service_account_coordinator(scheme, credential, options)
262
+
263
+ # Start the authentication flow
264
+ coordinator.start
265
+
266
+ # For service accounts, authentication is non-interactive, so the result should be available immediately
267
+ if coordinator.complete?
268
+ return coordinator.result if coordinator.success?
269
+
270
+ raise coordinator.error || Legate::Auth::Error.new('Service account authentication failed')
271
+
272
+ end
273
+
274
+ # This should never happen for service accounts
275
+ raise Legate::Auth::Error.new('Unexpected state: service account authentication requires interaction')
276
+ end
277
+ end
278
+ end
279
+ end
@@ -0,0 +1,125 @@
1
+ # File: lib/legate/auth/scheme.rb
2
+ # frozen_string_literal: true
3
+
4
+ require 'uri'
5
+ require_relative 'url_guard'
6
+
7
+ module Legate
8
+ module Auth
9
+ # Base class for all authentication schemes.
10
+ # Schemes provide logic for applying authentication to requests,
11
+ # refreshing tokens, and other operations specific to their authentication type.
12
+ class Scheme
13
+ # Get the type of authentication scheme
14
+ # @return [Symbol] The scheme type identifier
15
+ def scheme_type
16
+ raise NotImplementedError, "#{self.class} must implement #scheme_type"
17
+ end
18
+
19
+ # Apply authentication to a request
20
+ # @param request [Hash] The request hash to modify
21
+ # @param credential [Legate::Auth::Credential, Legate::Auth::ExchangedCredential] The credential to use
22
+ # @return [Hash] The modified request with authentication applied
23
+ def apply_to_request(request, credential)
24
+ raise NotImplementedError, "#{self.class} must implement #apply_to_request"
25
+ end
26
+
27
+ # Check if this scheme supports token refresh
28
+ # @return [Boolean] True if this scheme supports token refresh
29
+ def supports_refresh?
30
+ false
31
+ end
32
+
33
+ # Refresh an authentication token
34
+ # @param token [Legate::Auth::ExchangedCredential] The token to refresh
35
+ # @param credential [Legate::Auth::Credential] The credential containing refresh parameters
36
+ # @return [Legate::Auth::ExchangedCredential] The refreshed token
37
+ # @raise [Legate::Auth::TokenRefreshError] If the token cannot be refreshed
38
+ def refresh_token(token, credential)
39
+ raise NotImplementedError, "#{self.class} does not support token refresh"
40
+ end
41
+
42
+ # Exchange a credential for a token
43
+ # @param credential [Legate::Auth::Credential] The credential to exchange
44
+ # @return [Legate::Auth::ExchangedCredential] The exchanged token
45
+ # @raise [Legate::Auth::TokenExchangeError] If the credential cannot be exchanged
46
+ def exchange_token(credential)
47
+ raise NotImplementedError, "#{self.class} does not support token exchange"
48
+ end
49
+
50
+ # Revoke a token
51
+ # @param token [Legate::Auth::ExchangedCredential] The token to revoke
52
+ # @param credential [Legate::Auth::Credential] The credential for revocation parameters
53
+ # @return [Boolean] True if the token was revoked successfully
54
+ # @raise [Legate::Auth::TokenRevokeError] If the token cannot be revoked
55
+ def revoke_token(token, credential)
56
+ raise NotImplementedError, "#{self.class} does not support token revocation"
57
+ end
58
+
59
+ # Validates the scheme configuration
60
+ # @raise [Legate::Auth::SchemeValidationError] If the scheme configuration is invalid
61
+ # @abstract
62
+ def validate!
63
+ raise NotImplementedError, 'Subclasses must implement validate!'
64
+ end
65
+
66
+ # Returns a hash representation of the scheme
67
+ # @return [Hash] A hash containing the scheme configuration
68
+ # @abstract
69
+ def to_h
70
+ { type: scheme_type }
71
+ end
72
+
73
+ # Returns a string representation of the scheme
74
+ # @return [String] A string representing the scheme
75
+ def to_s
76
+ "#{self.class.name}<#{scheme_type}>"
77
+ end
78
+
79
+ # Builds an authorization URI for interactive authentication flows
80
+ # @param config [Legate::Auth::Config] The authentication configuration
81
+ # @param redirect_uri [String, nil] The redirect URI for the authorization request
82
+ # @param state [String, nil] A state parameter for the authorization request
83
+ # @return [String, nil] The authorization URI, or nil if not applicable
84
+ # @abstract
85
+ def build_authorization_uri(_config, _redirect_uri = nil, _state = nil)
86
+ nil # No-op in base class, override in subclasses that support interactive flows
87
+ end
88
+
89
+ # Checks if a response indicates an authentication error
90
+ # @param response [Hash] The HTTP response to check
91
+ # @return [Boolean] True if the response indicates an authentication error
92
+ def authentication_error?(response)
93
+ return false unless response.is_a?(Hash)
94
+
95
+ # HTTP status codes for auth errors (401 Unauthorized, 403 Forbidden)
96
+ [401, 403].include?(response[:status])
97
+ end
98
+
99
+ private
100
+
101
+ # Validates that a value is safe to use in an HTTP header.
102
+ # Rejects values containing CR, LF, or null bytes to prevent header injection.
103
+ # @param value [String] The header value to validate
104
+ # @param label [String] A label for error messages (e.g., "Bearer token")
105
+ # @raise [Legate::Auth::Error] If the value contains unsafe characters
106
+ def validate_header_value!(value, label = 'credential')
107
+ return unless value.is_a?(String)
108
+
109
+ return unless value.match?(/[\r\n\0]/)
110
+
111
+ raise Legate::Auth::Error, "#{label} contains invalid characters (CR, LF, or null byte)"
112
+ end
113
+
114
+ # Validates that an auth URL does not point to private/restricted network
115
+ # addresses. Delegates to the canonical {Legate::Auth::UrlGuard} so schemes
116
+ # and the web credential-test routes share one SSRF policy.
117
+ # @param url [String] The URL to validate
118
+ # @param label [String] A label for error messages
119
+ # @raise [Legate::Auth::Error] If the URL resolves to a restricted address
120
+ def validate_auth_url!(url, label: 'Auth URL')
121
+ Legate::Auth::UrlGuard.validate!(url, label: label)
122
+ end
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,212 @@
1
+ # File: lib/legate/auth/schemes/api_key.rb
2
+ # frozen_string_literal: true
3
+
4
+ require_relative '../scheme'
5
+ require_relative '../error'
6
+ require_relative '../exchanged_credential'
7
+
8
+ module Legate
9
+ module Auth
10
+ module Schemes
11
+ # API Key authentication scheme.
12
+ # This scheme applies an API key to requests via a header, query parameter, or cookie.
13
+ class ApiKey < Scheme
14
+ # Default header name for API key authentication
15
+ DEFAULT_HEADER_NAME = 'X-API-Key'
16
+
17
+ # Get the type of authentication scheme
18
+ # @return [Symbol] The scheme type identifier
19
+ def scheme_type
20
+ :api_key
21
+ end
22
+
23
+ # Apply authentication to a request
24
+ # @param request [Hash] The request hash to modify
25
+ # @param credential [Legate::Auth::Credential, Legate::Auth::ExchangedCredential] The credential to use
26
+ # @return [Hash] The modified request with authentication applied
27
+ # @raise [Legate::Auth::Error] If the API key cannot be applied
28
+ def apply_to_request(request, credential)
29
+ # Create a deep copy of the request to avoid modifying the original
30
+ request_copy = Marshal.load(Marshal.dump(request))
31
+
32
+ # Handle the case where we get a stack object from Excon
33
+ if request_copy.is_a?(Hash)
34
+ if request_copy[:stack]
35
+ # Extract the data from stack (Excon middleware format)
36
+ %i[scheme method path host port query].each do |key|
37
+ request_copy[key] = request_copy[:stack][key] if request_copy[:stack][key] && !request_copy[key]
38
+ end
39
+ end
40
+
41
+ # Ensure headers hash exists
42
+ request_copy[:headers] ||= {}
43
+ end
44
+
45
+ # Extract the API key from the credential
46
+ api_key = extract_api_key(credential)
47
+ raise Legate::Auth::Error, 'API key not found in credential' unless api_key
48
+
49
+ # Get parameters for applying the API key
50
+ location = credential[:location] || 'header'
51
+ name = credential[:name] || DEFAULT_HEADER_NAME
52
+
53
+ # Apply the API key based on location
54
+ case location.to_s.downcase
55
+ when 'header'
56
+ apply_to_header(request_copy, name, api_key)
57
+ when 'query', 'querystring'
58
+ apply_to_query(request_copy, name, api_key)
59
+ when 'cookie'
60
+ apply_to_cookie(request_copy, name, api_key)
61
+ else
62
+ raise Legate::Auth::Error, "Unsupported API key location: #{location}"
63
+ end
64
+ end
65
+
66
+ # Exchange a credential for a token
67
+ # @param credential [Legate::Auth::Credential] The credential to exchange
68
+ # @return [Legate::Auth::ExchangedCredential] The exchanged token
69
+ def exchange_token(credential)
70
+ # For API keys, we simply create a "token" that wraps the API key
71
+ # This is useful for token management consistency
72
+ api_key = extract_api_key(credential)
73
+ raise Legate::Auth::TokenExchangeError, 'API key not found in credential' unless api_key
74
+
75
+ # Create a simple exchanged credential that never expires
76
+ Legate::Auth::ExchangedCredential.new(
77
+ auth_type: :api_key,
78
+ api_key: api_key,
79
+ location: credential[:location] || 'header',
80
+ name: credential[:name] || DEFAULT_HEADER_NAME
81
+ )
82
+ end
83
+
84
+ # Get hash representation of the scheme
85
+ # @return [Hash] Scheme configuration as a hash
86
+ def to_h
87
+ {
88
+ type: scheme_type
89
+ }
90
+ end
91
+
92
+ private
93
+
94
+ # Extract the API key from a credential
95
+ # @param credential [Legate::Auth::Credential, Legate::Auth::ExchangedCredential] The credential
96
+ # @return [String, nil] The API key or nil if not found
97
+ def extract_api_key(credential)
98
+ # First try api_key
99
+ return credential[:api_key] if credential[:api_key]
100
+
101
+ # Next try key
102
+ return credential[:key] if credential[:key]
103
+
104
+ # Finally try token
105
+ credential[:token]
106
+ end
107
+
108
+ # Apply the API key to a request header
109
+ # @param request [Hash] The request to modify
110
+ # @param name [String] The header name
111
+ # @param api_key [String] The API key
112
+ # @return [Hash] The modified request
113
+ def apply_to_header(request, name, api_key)
114
+ validate_header_value!(api_key, 'API key')
115
+ validate_header_value!(name, 'Header name')
116
+ request[:headers] ||= {}
117
+ request[:headers][name] = api_key
118
+ request
119
+ end
120
+
121
+ # Apply the API key to a request query parameter
122
+ # @param request [Hash] The request to modify
123
+ # @param name [String] The parameter name
124
+ # @param api_key [String] The API key
125
+ # @return [Hash] The modified request
126
+ def apply_to_query(request, name, api_key)
127
+ # Initialize headers if not present
128
+ request[:headers] ||= {}
129
+
130
+ # The proper way to handle query parameters depends on the format
131
+ # expected by the HTTP client library
132
+
133
+ # Handle simple URL case
134
+ if request[:url]
135
+ # Properly append the query parameter
136
+ separator = request[:url].include?('?') ? '&' : '?'
137
+ request[:url] = "#{request[:url]}#{separator}#{name}=#{api_key}"
138
+ end
139
+
140
+ # Handle query param hash (used by Excon and other libraries)
141
+ if request[:query].is_a?(Hash)
142
+ # Simply add the param to the hash
143
+ request[:query][name] = api_key
144
+ elsif request[:query].is_a?(String)
145
+ # Append to existing query string
146
+ separator = request[:query].empty? ? '' : '&'
147
+ request[:query] = "#{request[:query]}#{separator}#{name}=#{api_key}"
148
+ elsif request[:query].nil?
149
+ # Create a new query hash
150
+ request[:query] = { name => api_key }
151
+ end
152
+
153
+ # Also update the URL with query parameters for debugging
154
+ if !request[:url] && (request[:scheme] || request[:host] || request[:path])
155
+ # Build the URL from components for reference
156
+ scheme = request[:scheme] || 'https'
157
+ host = request[:host] || 'example.com'
158
+ path = request[:path] || '/'
159
+ port = request[:port]
160
+
161
+ # Construct URL from components
162
+ port_part = port ? ":#{port}" : ''
163
+ url = "#{scheme}://#{host}#{port_part}#{path}"
164
+
165
+ # Add query string if present
166
+ if request[:query]
167
+ query_str = if request[:query].is_a?(Hash)
168
+ params = []
169
+ request[:query].each do |k, v|
170
+ params << "#{k}=#{v}"
171
+ end
172
+ params.join('&')
173
+ else
174
+ request[:query].to_s
175
+ end
176
+
177
+ url += "?#{query_str}" unless query_str.empty?
178
+ end
179
+
180
+ request[:url] = url
181
+ end
182
+
183
+ request
184
+ end
185
+
186
+ # Apply the API key to a request cookie
187
+ # @param request [Hash] The request to modify
188
+ # @param name [String] The cookie name
189
+ # @param api_key [String] The API key
190
+ # @return [Hash] The modified request
191
+ def apply_to_cookie(request, name, api_key)
192
+ validate_header_value!(api_key, 'API key (cookie)')
193
+ validate_header_value!(name, 'Cookie name')
194
+ # Initialize headers if not present
195
+ request[:headers] ||= {}
196
+
197
+ # Construct the cookie
198
+ cookie_value = "#{name}=#{api_key}"
199
+
200
+ # Append to existing cookie or set new one
201
+ request[:headers]['Cookie'] = if request[:headers]['Cookie'] && !request[:headers]['Cookie'].empty?
202
+ "#{request[:headers]['Cookie']}; #{cookie_value}"
203
+ else
204
+ cookie_value
205
+ end
206
+
207
+ request
208
+ end
209
+ end
210
+ end
211
+ end
212
+ end