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,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../coordinator'
4
+ require_relative '../schemes/openid_connect'
5
+ require_relative 'oauth2_coordinator'
6
+
7
+ module Legate
8
+ module Auth
9
+ module Coordinators
10
+ # OIDCCoordinator handles the interactive OpenID Connect authentication flow using fibers.
11
+ # It extends the OAuth2Coordinator with OIDC-specific functionality.
12
+ class OIDCCoordinator < OAuth2Coordinator
13
+ # Initialize a new OIDC coordinator
14
+ # @param scheme [Legate::Auth::Schemes::OpenIDConnect] The OIDC scheme
15
+ # @param credential [Legate::Auth::Credential] The credential with client information
16
+ # @param session_service [Legate::SessionService::Base] The session service
17
+ # @param token_store [Legate::Auth::TokenStore, nil] Optional token store
18
+ # @param timeout [Integer, nil] Optional timeout in seconds
19
+ # @param redirect_uri [String, nil] Optional redirect URI
20
+ def initialize(scheme:, credential:, session_service:, token_store: nil, timeout: DEFAULT_TIMEOUT, redirect_uri: nil)
21
+ super
22
+
23
+ raise ArgumentError, "Expected an OIDC scheme, got #{scheme.class}" unless scheme.is_a?(Legate::Auth::Schemes::OIDC)
24
+
25
+ return if credential.auth_type == :oidc
26
+ # Allow OAuth2 credentials as they are compatible
27
+ return if credential.auth_type == :oauth2
28
+
29
+ raise ArgumentError, "Credential must have auth_type :oidc or :oauth2, got #{credential.auth_type}"
30
+ end
31
+
32
+ protected
33
+
34
+ # Implement the OIDC authentication flow, extending the OAuth2 flow
35
+ # @return [Legate::Auth::ExchangedCredential] The authenticated credential
36
+ # @raise [Legate::Auth::Error] If authentication fails
37
+ def authenticate
38
+ # Call the parent (OAuth2) authentication flow first
39
+ oauth2_result = super
40
+
41
+ # For OIDC, we may want to request the userinfo endpoint
42
+ # if the ID token doesn't have all needed claims
43
+ if @scheme.should_fetch_userinfo? && oauth2_result
44
+ fetch_userinfo(oauth2_result)
45
+ else
46
+ oauth2_result
47
+ end
48
+ end
49
+
50
+ private
51
+
52
+ # Fetch additional user information using the userinfo endpoint
53
+ # @param token [Legate::Auth::ExchangedCredential] The token with access_token
54
+ # @return [Legate::Auth::ExchangedCredential] The token with added userinfo
55
+ # @raise [Legate::Auth::Error] If userinfo fetch fails
56
+ def fetch_userinfo(token)
57
+ # Fetch userinfo using the access token
58
+ userinfo = @scheme.fetch_userinfo(token)
59
+
60
+ # Add the userinfo to the token metadata
61
+ token.with(
62
+ metadata: (token.metadata || {}).merge(userinfo: userinfo)
63
+ )
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../coordinator'
4
+ require_relative '../schemes/service_account'
5
+ require_relative '../config'
6
+
7
+ module Legate
8
+ module Auth
9
+ module Coordinators
10
+ # ServiceAccountCoordinator handles non-interactive service account authentication
11
+ # with automatic token exchange and refresh. Unlike OAuth2 coordinators, service
12
+ # account authentication does not require user interaction.
13
+ class ServiceAccountCoordinator < Coordinator
14
+ # Authentication steps for Service Accounts
15
+ module Steps
16
+ TOKEN_EXCHANGE = :token_exchange
17
+ TOKEN_REFRESH = :token_refresh
18
+ end
19
+
20
+ # Initialize a new Service Account coordinator
21
+ # @param scheme [Legate::Auth::Schemes::ServiceAccount] The Service Account scheme
22
+ # @param credential [Legate::Auth::Credential] The credential with service account information
23
+ # @param session_service [Legate::SessionService::Base] The session service
24
+ # @param token_store [Legate::Auth::TokenStore, nil] Optional token store
25
+ # @param timeout [Integer, nil] Optional timeout in seconds
26
+ def initialize(scheme:, credential:, session_service:, token_store: nil, timeout: DEFAULT_TIMEOUT)
27
+ super(scheme: scheme, credential: credential, session_service: session_service, token_store: token_store, timeout: timeout)
28
+
29
+ raise ArgumentError, "Expected a ServiceAccount scheme, got #{scheme.class}" unless scheme.is_a?(Legate::Auth::Schemes::ServiceAccount)
30
+
31
+ raise ArgumentError, "Credential must have auth_type :service_account, got #{credential.auth_type}" unless credential.auth_type.to_sym == :service_account
32
+
33
+ @current_step = Steps::TOKEN_EXCHANGE
34
+ end
35
+
36
+ protected
37
+
38
+ # Implement the Service Account authentication flow
39
+ # @return [Legate::Auth::ExchangedCredential] The authenticated credential
40
+ # @raise [Legate::Auth::Error] If authentication fails
41
+ def authenticate
42
+ # Service account authentication is non-interactive, so we just exchange tokens
43
+ @current_step = Steps::TOKEN_EXCHANGE
44
+ exchanged_token = exchange_token
45
+
46
+ # Store the token if we have a token store
47
+ store_token(exchanged_token) if @token_store
48
+
49
+ exchanged_token
50
+ end
51
+
52
+ # Refresh an existing token
53
+ # @param token [Legate::Auth::ExchangedCredential] The token to refresh
54
+ # @return [Legate::Auth::ExchangedCredential] The refreshed token
55
+ # @raise [Legate::Auth::TokenRefreshError] If token refresh fails
56
+ def refresh(token)
57
+ @current_step = Steps::TOKEN_REFRESH
58
+
59
+ # For service accounts, we get a new token rather than refreshing the existing one
60
+ refreshed_token = @scheme.refresh_token(token, @credential)
61
+
62
+ # Store the refreshed token if we have a token store
63
+ store_token(refreshed_token) if @token_store
64
+
65
+ refreshed_token
66
+ end
67
+
68
+ private
69
+
70
+ # Exchange for a token
71
+ # @return [Legate::Auth::ExchangedCredential] The exchanged token
72
+ # @raise [Legate::Auth::TokenExchangeError] If token exchange fails
73
+ def exchange_token
74
+ # Create a config for token exchange, if needed by the scheme
75
+ auth_config = Legate::Auth::Config.new(
76
+ scheme: @scheme,
77
+ credential: @credential,
78
+ options: { request_id: @request_id }
79
+ )
80
+
81
+ # Exchange for tokens using the scheme
82
+ # Some service account implementations might need the config, others might not
83
+ if @scheme.method(:exchange_token).arity == 2
84
+ @scheme.exchange_token(auth_config, @credential)
85
+ else
86
+ @scheme.exchange_token(@credential)
87
+ end
88
+ end
89
+
90
+ # Store a token in the token store
91
+ # @param token [Legate::Auth::ExchangedCredential] The token to store
92
+ def store_token(token)
93
+ return unless @token_store && token
94
+
95
+ # Generate a key for the token
96
+ key = generate_token_key
97
+
98
+ # Store the token with the generated key
99
+ @token_store.store(key, token)
100
+ end
101
+
102
+ # Generate a key for storing the token
103
+ # @return [String] The generated key
104
+ def generate_token_key
105
+ # Create a base key using the scheme type, client email, and scopes
106
+ base_key = "#{@scheme.scheme_type}"
107
+
108
+ # Add client email if available
109
+ base_key += ":#{@credential[:client_email]}" if @credential[:client_email]
110
+
111
+ # Add scopes if available
112
+ base_key += ":#{@scheme.scopes.join(',')}" if @scheme.scopes && !@scheme.scopes.empty?
113
+
114
+ # Add audience if available and scopes aren't set
115
+ base_key += ":#{@scheme.audience}" if @scheme.audience && (@scheme.scopes.nil? || @scheme.scopes.empty?)
116
+
117
+ base_key
118
+ end
119
+ end
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,157 @@
1
+ # File: lib/legate/auth/credential.rb
2
+ # frozen_string_literal: true
3
+
4
+ module Legate
5
+ module Auth
6
+ # Represents authentication credentials required by different schemes.
7
+ # Handles different types of credentials such as API keys, OAuth2 client credentials,
8
+ # service account keys, and more.
9
+ #
10
+ # @example API Key credential
11
+ # credential = Legate::Auth::Credential.new(
12
+ # auth_type: :api_key,
13
+ # api_key: 'my-api-key'
14
+ # )
15
+ #
16
+ # @example OAuth2 credential with environment variable
17
+ # credential = Legate::Auth::Credential.new(
18
+ # auth_type: :oauth2,
19
+ # client_id: 'my-client-id',
20
+ # client_secret: 'ENV:MY_CLIENT_SECRET'
21
+ # )
22
+ class Credential
23
+ # Valid credential types
24
+ VALID_TYPES = %i[api_key oauth2 oidc service_account google_service_account http_bearer basic].freeze
25
+
26
+ # Prefix for environment variable references
27
+ ENV_PREFIX = 'ENV:'
28
+
29
+ # @return [Symbol] The type of authentication
30
+ attr_reader :auth_type
31
+
32
+ # Initialize a new credential
33
+ # @param auth_type [Symbol] The type of authentication (:api_key, :oauth2, :oidc, :service_account, :http_bearer)
34
+ # @param kwargs [Hash] Additional attributes for the specific auth type
35
+ # @raise [Legate::Auth::CredentialError] If the credential is invalid
36
+ def initialize(auth_type:, **kwargs)
37
+ @auth_type = auth_type.to_sym
38
+ @attributes = kwargs
39
+
40
+ validate_auth_type!
41
+ validate_required_attributes!
42
+ end
43
+
44
+ # Get an attribute value
45
+ # @param name [Symbol, String] The attribute name
46
+ # @param resolve_env [Boolean] Whether to resolve environment variables
47
+ # @return [Object, nil] The attribute value, or nil if not present
48
+ # @raise [Legate::Auth::EnvironmentVariableNotFoundError] If an environment variable is not found
49
+ def [](name, resolve_env: true)
50
+ attr_name = name.to_sym
51
+ value = @attributes[attr_name]
52
+
53
+ if resolve_env && value.is_a?(String) && value.start_with?(ENV_PREFIX)
54
+ resolve_environment_variable(value)
55
+ else
56
+ value
57
+ end
58
+ end
59
+
60
+ # Set an attribute value
61
+ # @param name [Symbol, String] The attribute name
62
+ # @param value [Object] The attribute value
63
+ def []=(name, value)
64
+ @attributes[name.to_sym] = value
65
+ end
66
+
67
+ # Convert to a hash
68
+ # @param resolve_env [Boolean] Whether to resolve environment variables
69
+ # @return [Hash] A hash representation of the credential
70
+ def to_h(resolve_env: false)
71
+ result = { auth_type: @auth_type }
72
+
73
+ @attributes.each do |key, value|
74
+ result[key] = if resolve_env && value.is_a?(String) && value.start_with?(ENV_PREFIX)
75
+ resolve_environment_variable(value)
76
+ else
77
+ value
78
+ end
79
+ end
80
+
81
+ result
82
+ end
83
+
84
+ # Check if the credential has an attribute
85
+ # @param name [Symbol, String] The attribute name
86
+ # @return [Boolean] True if the attribute exists
87
+ def has_attribute?(name)
88
+ @attributes.key?(name.to_sym)
89
+ end
90
+
91
+ private
92
+
93
+ # Validate the authentication type
94
+ # @raise [Legate::Auth::CredentialError] If the authentication type is invalid
95
+ def validate_auth_type!
96
+ return if VALID_TYPES.include?(@auth_type)
97
+
98
+ raise Legate::Auth::CredentialError,
99
+ "Invalid auth_type: #{@auth_type}. Must be one of: #{VALID_TYPES.join(', ')}"
100
+ end
101
+
102
+ # Validate required attributes based on the authentication type
103
+ # @raise [Legate::Auth::CredentialError] If required attributes are missing
104
+ def validate_required_attributes!
105
+ required_attrs = required_attributes_for_type
106
+ missing_attrs = required_attrs.reject { |attr| @attributes.key?(attr) }
107
+
108
+ return if missing_attrs.empty?
109
+
110
+ raise Legate::Auth::CredentialError,
111
+ "Missing required attributes for #{@auth_type}: #{missing_attrs.join(', ')}"
112
+ end
113
+
114
+ # Required attributes based on the authentication type
115
+ # @return [Array<Symbol>] The required attributes
116
+ def required_attributes_for_type
117
+ case @auth_type
118
+ when :api_key
119
+ [:api_key]
120
+ when :oauth2
121
+ [:client_id]
122
+ when :oidc
123
+ [:client_id]
124
+ when :service_account
125
+ # Allow either service_account_key or service_account_key_file
126
+ return [] if @attributes.key?(:service_account_key) || @attributes.key?(:service_account_key_file)
127
+
128
+ [:service_account_key]
129
+ when :google_service_account
130
+ # Allow either service_account_key or service_account_key_file
131
+ return [] if @attributes.key?(:service_account_key) || @attributes.key?(:service_account_key_file)
132
+
133
+ [:service_account_key]
134
+ when :http_bearer
135
+ [:bearer_token]
136
+ when :basic
137
+ %i[username password]
138
+ else
139
+ []
140
+ end
141
+ end
142
+
143
+ # Resolve an environment variable reference
144
+ # @param value [String] The environment variable reference (e.g., "ENV:VARIABLE_NAME")
145
+ # @return [String] The resolved value
146
+ # @raise [Legate::Auth::EnvironmentVariableNotFoundError] If the environment variable is not found
147
+ def resolve_environment_variable(value)
148
+ env_name = value[ENV_PREFIX.length..-1]
149
+ env_value = ENV[env_name]
150
+
151
+ raise Legate::Auth::EnvironmentVariableNotFoundError, env_name if env_value.nil? || env_value.empty?
152
+
153
+ env_value
154
+ end
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,108 @@
1
+ # File: lib/legate/auth/encryption.rb
2
+ # frozen_string_literal: true
3
+
4
+ module Legate
5
+ module Auth
6
+ # Provides encryption and decryption utilities for sensitive authentication data.
7
+ # Uses the rbnacl gem for authenticated encryption.
8
+ module Encryption
9
+ # Environment variable name for the encryption key
10
+ ENV_KEY_NAME = 'LEGATE_AUTH_ENCRYPTION_KEY'
11
+
12
+ # Header added to encrypted data for identification
13
+ ENCRYPTION_HEADER = 'LGTAUTH'
14
+
15
+ class << self
16
+ # Encrypts sensitive data
17
+ # @param data [String] The data to encrypt
18
+ # @param key [String, nil] The encryption key (defaults to the key from environment)
19
+ # @return [String] The encrypted data in Base64 format with header
20
+ # @raise [LoadError] If the rbnacl gem is not available
21
+ # @raise [ArgumentError] If the encryption key is not available
22
+ def encrypt(data, key = nil)
23
+ require_rbnacl
24
+ encryption_key = key || get_encryption_key
25
+
26
+ require 'base64'
27
+ box = create_box(encryption_key)
28
+ encrypted = box.encrypt(data.to_s)
29
+ "#{ENCRYPTION_HEADER}#{Base64.strict_encode64(encrypted)}"
30
+ end
31
+
32
+ # Decrypts sensitive data
33
+ # @param encrypted_data [String] The encrypted data to decrypt
34
+ # @param key [String, nil] The encryption key (defaults to the key from environment)
35
+ # @return [String] The decrypted data
36
+ # @raise [LoadError] If the rbnacl gem is not available
37
+ # @raise [ArgumentError] If the data is not in the expected format or the key is invalid
38
+ def decrypt(encrypted_data, key = nil)
39
+ require_rbnacl
40
+ encryption_key = key || get_encryption_key
41
+
42
+ # Check format and remove header
43
+ raise ArgumentError, 'Invalid encrypted data format' unless encrypted_data.to_s.start_with?(ENCRYPTION_HEADER)
44
+
45
+ encoded = encrypted_data.to_s[ENCRYPTION_HEADER.length..-1]
46
+ require 'base64'
47
+ encrypted = Base64.strict_decode64(encoded)
48
+
49
+ box = create_box(encryption_key)
50
+ box.decrypt(encrypted)
51
+ rescue RbNaCl::CryptoError => e
52
+ raise ArgumentError, "Decryption failed: #{e.message}"
53
+ rescue ArgumentError => e
54
+ raise ArgumentError, "Invalid Base64 encoding: #{e.message}"
55
+ end
56
+
57
+ # Generates a new random encryption key
58
+ # @return [String] A new encryption key in Base64 format
59
+ # @raise [LoadError] If the rbnacl gem is not available
60
+ def generate_key
61
+ require_rbnacl
62
+ require 'base64'
63
+ raw_key = RbNaCl::Random.random_bytes(RbNaCl::SecretBox.key_bytes)
64
+ Base64.strict_encode64(raw_key)
65
+ end
66
+
67
+ # Checks if the encrypted data is in the expected format
68
+ # @param data [String] The data to check
69
+ # @return [Boolean] True if the data appears to be encrypted
70
+ def encrypted?(data)
71
+ data.to_s.start_with?(ENCRYPTION_HEADER)
72
+ end
73
+
74
+ private
75
+
76
+ # Gets the encryption key from the environment or configuration
77
+ # @return [String] The encryption key in raw binary format
78
+ # @raise [ArgumentError] If the encryption key is not available
79
+ def get_encryption_key
80
+ env_key = ENV[ENV_KEY_NAME]
81
+ raise ArgumentError, "Encryption key not found. Set #{ENV_KEY_NAME} environment variable." unless env_key
82
+
83
+ require 'base64'
84
+ begin
85
+ Base64.strict_decode64(env_key)
86
+ rescue ArgumentError
87
+ raise ArgumentError, 'Invalid encryption key format. Must be Base64-encoded.'
88
+ end
89
+ end
90
+
91
+ # Creates a SimpleBox from the encryption key
92
+ # @param key [String] The encryption key in raw binary format
93
+ # @return [RbNaCl::SimpleBox] A box for encryption/decryption
94
+ def create_box(key)
95
+ RbNaCl::SimpleBox.from_secret_key(key)
96
+ end
97
+
98
+ # Ensures that the rbnacl gem is available
99
+ # @raise [LoadError] If the rbnacl gem is not available
100
+ def require_rbnacl
101
+ require 'rbnacl'
102
+ rescue LoadError
103
+ raise LoadError, "rbnacl gem is required for encryption. Add it to your Gemfile: gem 'rbnacl'"
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,94 @@
1
+ # File: lib/legate/auth/error.rb
2
+ # frozen_string_literal: true
3
+
4
+ module Legate
5
+ module Auth
6
+ # Base class for all authentication-related errors
7
+ class Error < Legate::Error
8
+ # @param message [String] The error message
9
+ # @param cause [Exception, nil] The underlying exception that caused this error
10
+ def initialize(message = nil, cause = nil)
11
+ message = "Authentication error#{": #{message}" if message}"
12
+ super(message)
13
+ set_backtrace(cause.backtrace) if cause && cause.backtrace
14
+ end
15
+ end
16
+
17
+ # Raised when authentication configuration is invalid
18
+ class ConfigurationError < Error
19
+ def initialize(message = nil, cause = nil)
20
+ super(message || 'Invalid authentication configuration', cause)
21
+ end
22
+ end
23
+
24
+ # Raised when a token exchange operation fails
25
+ class TokenExchangeError < Error
26
+ # @param message [String] The error message
27
+ # @param provider_error [String, nil] Error information from the provider
28
+ # @param cause [Exception, nil] The underlying exception that caused this error
29
+ def initialize(message = nil, provider_error = nil, cause = nil)
30
+ error_message = message || 'Token exchange failed'
31
+ error_message = "#{error_message}: #{provider_error}" if provider_error
32
+ super(error_message, cause)
33
+ end
34
+ end
35
+
36
+ # Raised when a token refresh operation fails
37
+ class TokenRefreshError < Error
38
+ # @param message [String] The error message
39
+ # @param provider_error [String, nil] Error information from the provider
40
+ # @param cause [Exception, nil] The underlying exception that caused this error
41
+ def initialize(message = nil, provider_error = nil, cause = nil)
42
+ error_message = message || 'Token refresh failed'
43
+ error_message = "#{error_message}: #{provider_error}" if provider_error
44
+ super(error_message, cause)
45
+ end
46
+ end
47
+
48
+ # Raised when a token revocation operation fails
49
+ class TokenRevocationError < Error
50
+ # @param message [String] The error message
51
+ # @param provider_error [String, nil] Error information from the provider
52
+ # @param cause [Exception, nil] The underlying exception that caused this error
53
+ def initialize(message = nil, provider_error = nil, cause = nil)
54
+ error_message = message || 'Token revocation failed'
55
+ error_message = "#{error_message}: #{provider_error}" if provider_error
56
+ super(error_message, cause)
57
+ end
58
+ end
59
+
60
+ # Raised when an authentication provider returns an error
61
+ class ProviderError < Error
62
+ # @param message [String] The error message
63
+ # @param provider_error [String, nil] Error information from the provider
64
+ # @param cause [Exception, nil] The underlying exception that caused this error
65
+ def initialize(message = nil, provider_error = nil, cause = nil)
66
+ error_message = message || 'Authentication provider error'
67
+ error_message = "#{error_message}: #{provider_error}" if provider_error
68
+ super(error_message, cause)
69
+ end
70
+ end
71
+
72
+ # Raised when credentials are missing or invalid
73
+ class CredentialError < Error
74
+ def initialize(message = nil, cause = nil)
75
+ super(message || 'Invalid or missing credentials', cause)
76
+ end
77
+ end
78
+
79
+ # Raised when an environment variable used for credentials is not found
80
+ class EnvironmentVariableNotFoundError < CredentialError
81
+ # @param var_name [String] The name of the environment variable
82
+ def initialize(var_name, cause = nil)
83
+ super("Environment variable not found: #{var_name}", cause)
84
+ end
85
+ end
86
+
87
+ # Raised when a scheme validation fails
88
+ class SchemeValidationError < ConfigurationError
89
+ def initialize(message = nil, cause = nil)
90
+ super(message || 'Invalid authentication scheme configuration', cause)
91
+ end
92
+ end
93
+ end
94
+ end