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,531 @@
1
+ # File: lib/legate/auth/manager.rb
2
+ # frozen_string_literal: true
3
+
4
+ require 'singleton'
5
+ require_relative '../errors'
6
+ require_relative 'error'
7
+ require_relative 'credential'
8
+ require_relative 'manager_store'
9
+ require_relative 'schemes/api_key'
10
+ require_relative 'schemes/http_bearer'
11
+ require_relative 'schemes/oauth2'
12
+ require_relative 'schemes/openid_connect'
13
+ require_relative 'schemes/service_account'
14
+ require_relative 'schemes/google_service_account'
15
+
16
+ module Legate
17
+ module Auth
18
+ # The AuthManager is a singleton that manages authentication schemes and credentials.
19
+ # It provides a centralized registry for authentication schemes and credentials,
20
+ # as well as methods for finding the appropriate scheme and credential for a given URL.
21
+ class Manager
22
+ include Singleton
23
+
24
+ # @return [Legate::Auth::ManagerStore::RedisStore, Legate::Auth::ManagerStore::InMemoryStore, nil]
25
+ attr_reader :store
26
+
27
+ # Read access to the registered schemes/credentials/url-mappings. Exposed so
28
+ # callers (e.g. the web routes) don't reach in via instance_variable_get.
29
+ # @return [Hash{Symbol => Legate::Auth::Scheme}]
30
+ attr_reader :schemes
31
+ # @return [Hash{Symbol => Legate::Auth::Credential}]
32
+ attr_reader :credentials
33
+ # @return [Array<Hash>]
34
+ attr_reader :url_mappings
35
+
36
+ def initialize
37
+ @schemes = {}
38
+ @credentials = {}
39
+ @url_mappings = []
40
+ @store = nil
41
+ @loaded_from_store = false
42
+
43
+ # Register built-in schemes
44
+ register_default_schemes
45
+ end
46
+
47
+ # Set the persistence store and load existing data
48
+ # @param store [Legate::Auth::ManagerStore::RedisStore, Legate::Auth::ManagerStore::InMemoryStore]
49
+ # @param load_immediately [Boolean] Whether to load data from store immediately (default: true)
50
+ def set_store(store, load_immediately: true)
51
+ @store = store
52
+ load_from_store if load_immediately && store&.available?
53
+ end
54
+
55
+ # Load all schemes, credentials, and URL mappings from the store
56
+ # @return [Boolean] true if successful
57
+ def load_from_store
58
+ return false unless @store&.available?
59
+ return true if @loaded_from_store # Don't reload if already loaded
60
+
61
+ Legate.logger&.info('Loading authentication configuration from store...')
62
+
63
+ # Load credentials first (schemes might depend on them for URL mappings)
64
+ load_credentials_from_store
65
+
66
+ # Load schemes (will overwrite defaults with stored config)
67
+ load_schemes_from_store
68
+
69
+ # Load URL mappings
70
+ load_url_mappings_from_store
71
+
72
+ @loaded_from_store = true
73
+ Legate.logger&.info('Authentication configuration loaded from store.')
74
+ true
75
+ rescue StandardError => e
76
+ Legate.logger&.error("Failed to load auth config from store: #{e.message}")
77
+ false
78
+ end
79
+
80
+ # Force reload from store (useful after external changes)
81
+ def reload_from_store
82
+ @loaded_from_store = false
83
+ load_from_store
84
+ end
85
+
86
+ # Register an authentication scheme
87
+ # @param scheme [Legate::Auth::Scheme] The scheme to register
88
+ # @param name [Symbol, String] Optional name for the scheme (defaults to scheme type)
89
+ # @param persist [Boolean] Whether to persist to store (default: true)
90
+ # @return [Symbol] The name the scheme was registered under
91
+ def register_scheme(scheme, name = nil, persist: true)
92
+ raise ArgumentError, 'Scheme must be an Legate::Auth::Scheme' unless scheme.is_a?(Legate::Auth::Scheme)
93
+
94
+ # Use scheme type as name if not provided
95
+ name ||= scheme.scheme_type
96
+ name = name.to_sym
97
+
98
+ @schemes[name] = scheme
99
+
100
+ # Persist to store if available and persistence is enabled
101
+ @store&.save_scheme(name, scheme) if persist && @store&.available?
102
+
103
+ name
104
+ end
105
+
106
+ # Unregister/delete a scheme
107
+ # @param name [Symbol, String] The scheme name
108
+ # @param persist [Boolean] Whether to persist the deletion (default: true)
109
+ # @return [Boolean] true if deleted
110
+ def unregister_scheme(name, persist: true)
111
+ name = name.to_sym
112
+ deleted = @schemes.delete(name)
113
+
114
+ @store&.delete_scheme(name) if persist && deleted && @store&.available?
115
+
116
+ !deleted.nil?
117
+ end
118
+
119
+ # Register a credential
120
+ # @param credential [Legate::Auth::Credential] The credential to register
121
+ # @param name [Symbol, String] The name to register the credential under
122
+ # @param persist [Boolean] Whether to persist to store (default: true)
123
+ # @return [Symbol] The name the credential was registered under
124
+ def register_credential(credential, name, persist: true)
125
+ raise ArgumentError, 'Credential must be an Legate::Auth::Credential' unless credential.is_a?(Legate::Auth::Credential)
126
+ raise ArgumentError, 'Name must be provided' if name.nil?
127
+
128
+ name = name.to_sym
129
+ @credentials[name] = credential
130
+
131
+ # Persist to store if available and persistence is enabled
132
+ @store&.save_credential(name, credential) if persist && @store&.available?
133
+
134
+ name
135
+ end
136
+
137
+ # Unregister/delete a credential
138
+ # @param name [Symbol, String] The credential name
139
+ # @param persist [Boolean] Whether to persist the deletion (default: true)
140
+ # @return [Boolean] true if deleted
141
+ def unregister_credential(name, persist: true)
142
+ name = name.to_sym
143
+ deleted = @credentials.delete(name)
144
+
145
+ @store&.delete_credential(name) if persist && deleted && @store&.available?
146
+
147
+ !deleted.nil?
148
+ end
149
+
150
+ # Register a URL mapping to a scheme and credential
151
+ # @param url_pattern [String, Regexp] The URL pattern to match
152
+ # @param scheme_name [Symbol, String] The name of the scheme to use
153
+ # @param credential_name [Symbol, String] The name of the credential to use
154
+ # @param persist [Boolean] Whether to persist to store (default: true)
155
+ def register_url_mapping(url_pattern, scheme_name, credential_name, persist: true)
156
+ scheme_name = scheme_name.to_sym
157
+ credential_name = credential_name.to_sym
158
+
159
+ raise ArgumentError, "Unknown scheme: #{scheme_name}" unless @schemes.key?(scheme_name)
160
+
161
+ raise ArgumentError, "Unknown credential: #{credential_name}" unless @credentials.key?(credential_name)
162
+
163
+ @url_mappings << {
164
+ pattern: url_pattern,
165
+ scheme_name: scheme_name,
166
+ credential_name: credential_name
167
+ }
168
+
169
+ # Persist all URL mappings to store
170
+ @store&.save_url_mappings(@url_mappings) if persist && @store&.available?
171
+ end
172
+
173
+ # Remove a URL mapping by index
174
+ # @param index [Integer] The index of the mapping to remove
175
+ # @param persist [Boolean] Whether to persist the deletion (default: true)
176
+ # @return [Boolean] true if removed
177
+ def remove_url_mapping(index, persist: true)
178
+ return false if index < 0 || index >= @url_mappings.size
179
+
180
+ @url_mappings.delete_at(index)
181
+
182
+ # Persist all URL mappings to store
183
+ @store&.save_url_mappings(@url_mappings) if persist && @store&.available?
184
+
185
+ true
186
+ end
187
+
188
+ # Replaces the full set of URL mappings and persists them. Used by the web
189
+ # UI, which manages a richer mapping shape (pattern/priority/active) than
190
+ # register_url_mapping and edits the array in place before saving.
191
+ # @param mappings [Array<Hash>] the new mapping set
192
+ # @param persist [Boolean] whether to persist (default: true)
193
+ # @return [Array<Hash>] the stored mappings
194
+ def replace_url_mappings(mappings, persist: true)
195
+ @url_mappings = mappings
196
+ @store&.save_url_mappings(@url_mappings) if persist && @store&.available?
197
+ @url_mappings
198
+ end
199
+
200
+ # Get a registered scheme
201
+ # @param name [Symbol, String] The name of the scheme
202
+ # @return [Legate::Auth::Scheme, nil] The scheme or nil if not found
203
+ def get_scheme(name)
204
+ @schemes[name.to_sym]
205
+ end
206
+
207
+ # Get a registered credential
208
+ # @param name [Symbol, String] The name of the credential
209
+ # @return [Legate::Auth::Credential, nil] The credential or nil if not found
210
+ def get_credential(name)
211
+ @credentials[name.to_sym]
212
+ end
213
+
214
+ # Find a scheme by type without requiring credentials
215
+ # @param scheme_type [Symbol, String] The scheme type to find
216
+ # @return [Legate::Auth::Scheme, nil] The scheme or nil if not found
217
+ def find_scheme(scheme_type)
218
+ scheme_sym = scheme_type.to_sym
219
+
220
+ # Try to find by exact scheme_type match first
221
+ scheme = @schemes.values.find { |s| s.scheme_type == scheme_sym }
222
+
223
+ # If not found by scheme_type, try to find by registration name
224
+ # This handles cases like :oidc -> OpenIDConnect where scheme_type is :openid_connect
225
+ scheme ||= @schemes[scheme_sym]
226
+
227
+ # Special mappings for backward compatibility and aliases
228
+ if scheme.nil?
229
+ scheme_mappings = {
230
+ oidc: :openid_connect,
231
+ openid_connect: :oidc
232
+ }
233
+
234
+ # Try the mapped scheme type
235
+ mapped_type = scheme_mappings[scheme_sym]
236
+ if mapped_type
237
+ scheme = @schemes.values.find { |s| s.scheme_type == mapped_type } ||
238
+ @schemes[mapped_type]
239
+ end
240
+ end
241
+
242
+ scheme
243
+ end
244
+
245
+ # Find the appropriate scheme and credential for a URL
246
+ # @param url [String] The URL to find a scheme and credential for
247
+ # @param scheme_type [Symbol, nil] Optional scheme type to filter by
248
+ # @param credential_name [Symbol, String, nil] Optional credential name to use
249
+ # @return [Array<Legate::Auth::Scheme, Legate::Auth::Credential>, nil] The scheme and credential, or nil if not found
250
+ def find_scheme_and_credential(url: nil, scheme_type: nil, credential_name: nil)
251
+ # Case 1: Direct credential and matching scheme specified
252
+ if credential_name
253
+ credential = get_credential(credential_name)
254
+ return nil unless credential
255
+
256
+ if scheme_type
257
+ # Find scheme of the specified type
258
+ scheme = @schemes.values.find { |s| s.scheme_type == scheme_type.to_sym }
259
+ return [scheme, credential] if scheme
260
+ else
261
+ # Try to find a compatible scheme for this credential
262
+ scheme = find_compatible_scheme(credential)
263
+ return [scheme, credential] if scheme
264
+ end
265
+ end
266
+
267
+ # Case 2: URL matching
268
+ if url
269
+ found_mapping = @url_mappings.find do |mapping|
270
+ pattern = mapping[:pattern]
271
+
272
+ # Check if URL matches the pattern
273
+ (pattern.is_a?(Regexp) && url =~ pattern) ||
274
+ (pattern.is_a?(String) && url.include?(pattern))
275
+ end
276
+
277
+ if found_mapping
278
+ scheme = get_scheme(found_mapping[:scheme_name])
279
+ credential = get_credential(found_mapping[:credential_name])
280
+
281
+ # Skip if we're filtering by scheme_type and it doesn't match
282
+ return nil if scheme_type && scheme.scheme_type != scheme_type.to_sym
283
+
284
+ return [scheme, credential] if scheme && credential
285
+ end
286
+
287
+ # No matching URL mapping found, if a URL was provided and no mapping matched
288
+ # but scheme_type was specified, we shouldn't continue to Case 3
289
+ return nil if scheme_type && url
290
+ end
291
+
292
+ # Case 3: Just trying to find by scheme_type with any credential
293
+ if scheme_type
294
+ scheme_sym = scheme_type.to_sym
295
+
296
+ # Try to find by exact scheme_type match first
297
+ scheme = @schemes.values.find { |s| s.scheme_type == scheme_sym }
298
+
299
+ # If not found by scheme_type, try to find by registration name
300
+ # This handles cases like :oidc -> OpenIDConnect where scheme_type is :openid_connect
301
+ scheme ||= @schemes[scheme_sym]
302
+
303
+ # Special mappings for backward compatibility and aliases
304
+ if scheme.nil?
305
+ scheme_mappings = {
306
+ oidc: :openid_connect,
307
+ openid_connect: :oidc
308
+ }
309
+
310
+ # Try the mapped scheme type
311
+ mapped_type = scheme_mappings[scheme_sym]
312
+ if mapped_type
313
+ scheme = @schemes.values.find { |s| s.scheme_type == mapped_type } ||
314
+ @schemes[mapped_type]
315
+ end
316
+ end
317
+
318
+ return nil unless scheme
319
+
320
+ # Find any compatible credential
321
+ @credentials.each_value do |cred|
322
+ return [scheme, cred] if credential_compatible_with_scheme?(cred, scheme)
323
+ end
324
+ end
325
+
326
+ nil
327
+ end
328
+
329
+ private
330
+
331
+ # Register the default built-in schemes
332
+ def register_default_schemes
333
+ register_scheme(Legate::Auth::Schemes::ApiKey.new, :api_key)
334
+ register_scheme(Legate::Auth::Schemes::HTTPBearer.new, :http_bearer)
335
+
336
+ # Set default values for OAuth2 and other schemes regardless of environment
337
+ oauth2 = Legate::Auth::Schemes::OAuth2.new(
338
+ authorization_url: 'https://example.com/oauth/authorize',
339
+ token_url: 'https://example.com/oauth/token'
340
+ )
341
+ register_scheme(oauth2, :oauth2)
342
+
343
+ oidc = Legate::Auth::Schemes::OpenIDConnect.new(
344
+ authorization_url: 'https://example.com/oidc/authorize',
345
+ token_url: 'https://example.com/oidc/token'
346
+ )
347
+ register_scheme(oidc, :oidc)
348
+
349
+ # Set test environment before creating service account schemes
350
+ # This allows the schemes to be created without requiring real credentials
351
+ original_env = ENV['RSPEC_ENV']
352
+ ENV['RSPEC_ENV'] = 'test'
353
+
354
+ begin
355
+ service_account = Legate::Auth::Schemes::ServiceAccount.new(
356
+ token_url: 'https://example.com/token'
357
+ )
358
+ register_scheme(service_account, :service_account)
359
+
360
+ google_service_account = Legate::Auth::Schemes::GoogleServiceAccount.new(
361
+ scopes: ['https://www.googleapis.com/auth/cloud-platform']
362
+ )
363
+ register_scheme(google_service_account, :google_service_account)
364
+ ensure
365
+ # Restore original environment
366
+ if original_env
367
+ ENV['RSPEC_ENV'] = original_env
368
+ else
369
+ ENV.delete('RSPEC_ENV')
370
+ end
371
+ end
372
+ end
373
+
374
+ # Find a compatible scheme for a credential
375
+ # @param credential [Legate::Auth::Credential] The credential
376
+ # @return [Legate::Auth::Scheme, nil] A compatible scheme or nil
377
+ def find_compatible_scheme(credential)
378
+ @schemes.each_value do |scheme|
379
+ return scheme if credential_compatible_with_scheme?(credential, scheme)
380
+ end
381
+ nil
382
+ end
383
+
384
+ # Check if a credential is compatible with a scheme
385
+ # @param credential [Legate::Auth::Credential] The credential
386
+ # @param scheme [Legate::Auth::Scheme] The scheme
387
+ # @return [Boolean] True if compatible
388
+ def credential_compatible_with_scheme?(credential, scheme)
389
+ scheme_type = scheme.scheme_type
390
+
391
+ case scheme_type
392
+ when :api_key
393
+ credential[:api_key, resolve_env: false]
394
+ when :http_bearer
395
+ # Check for bearer token or basic auth credentials
396
+ credential[:bearer_token, resolve_env: false] ||
397
+ (credential[:username, resolve_env: false] && credential[:password, resolve_env: false])
398
+ when :oauth2, :oidc, :openid_connect
399
+ credential[:client_id, resolve_env: false] &&
400
+ credential[:client_secret, resolve_env: false]
401
+ when :service_account, :google_service_account
402
+ # For service accounts, check for either service_account_key or individual fields
403
+ credential[:service_account_key, resolve_env: false] ||
404
+ (credential[:client_email, resolve_env: false] &&
405
+ credential[:private_key, resolve_env: false])
406
+ when :basic
407
+ credential[:username, resolve_env: false] &&
408
+ credential[:password, resolve_env: false]
409
+ else
410
+ false
411
+ end
412
+ end
413
+
414
+ # Load credentials from store
415
+ def load_credentials_from_store
416
+ return unless @store&.available?
417
+
418
+ stored_credentials = @store.load_all_credentials
419
+ stored_credentials.each do |name, data|
420
+ credential = deserialize_credential(data)
421
+ @credentials[name] = credential if credential
422
+ rescue StandardError => e
423
+ Legate.logger&.warn("Failed to load credential '#{name}': #{e.message}")
424
+ end
425
+
426
+ Legate.logger&.debug("Loaded #{stored_credentials.size} credentials from store")
427
+ end
428
+
429
+ # Load schemes from store
430
+ def load_schemes_from_store
431
+ return unless @store&.available?
432
+
433
+ stored_schemes = @store.load_all_schemes
434
+ stored_schemes.each do |name, data|
435
+ scheme = deserialize_scheme(data)
436
+ @schemes[name] = scheme if scheme
437
+ rescue StandardError => e
438
+ Legate.logger&.warn("Failed to load scheme '#{name}': #{e.message}")
439
+ end
440
+
441
+ Legate.logger&.debug("Loaded #{stored_schemes.size} schemes from store")
442
+ end
443
+
444
+ # Load URL mappings from store
445
+ def load_url_mappings_from_store
446
+ return unless @store&.available?
447
+
448
+ @url_mappings = @store.load_url_mappings
449
+ Legate.logger&.debug("Loaded #{@url_mappings.size} URL mappings from store")
450
+ end
451
+
452
+ # Deserialize a credential from stored data
453
+ # @param data [Hash] The stored credential data
454
+ # @return [Legate::Auth::Credential, nil]
455
+ def deserialize_credential(data)
456
+ return nil unless data && data[:auth_type]
457
+
458
+ auth_type = data[:auth_type].to_sym
459
+ attributes = data.reject { |k, _| k == :auth_type }
460
+
461
+ Legate::Auth::Credential.new(auth_type: auth_type, **attributes)
462
+ rescue StandardError => e
463
+ Legate.logger&.warn("Failed to deserialize credential: #{e.message}")
464
+ nil
465
+ end
466
+
467
+ # Deserialize a scheme from stored data
468
+ # @param data [Hash] The stored scheme data
469
+ # @return [Legate::Auth::Scheme, nil]
470
+ def deserialize_scheme(data)
471
+ return nil unless data && data[:scheme_type]
472
+
473
+ scheme_type = data[:scheme_type].to_sym
474
+
475
+ case scheme_type
476
+ when :api_key
477
+ Legate::Auth::Schemes::ApiKey.new(
478
+ header_name: data[:header_name],
479
+ query_param_name: data[:query_param_name],
480
+ location: data[:location]&.to_sym
481
+ )
482
+ when :http_bearer
483
+ Legate::Auth::Schemes::HTTPBearer.new
484
+ when :oauth2
485
+ Legate::Auth::Schemes::OAuth2.new(
486
+ authorization_url: data[:authorization_url] || 'https://example.com/oauth/authorize',
487
+ token_url: data[:token_url] || 'https://example.com/oauth/token',
488
+ scopes: data[:scopes],
489
+ use_pkce: data[:use_pkce],
490
+ revocation_url: data[:revocation_url]
491
+ )
492
+ when :openid_connect, :oidc
493
+ Legate::Auth::Schemes::OpenIDConnect.new(
494
+ authorization_url: data[:authorization_url] || 'https://example.com/oidc/authorize',
495
+ token_url: data[:token_url] || 'https://example.com/oidc/token',
496
+ scopes: data[:scopes],
497
+ use_pkce: data[:use_pkce],
498
+ revocation_url: data[:revocation_url]
499
+ )
500
+ when :service_account
501
+ # Temporarily set test env to allow scheme creation without real credentials
502
+ original_env = ENV['RSPEC_ENV']
503
+ ENV['RSPEC_ENV'] = 'test'
504
+ begin
505
+ Legate::Auth::Schemes::ServiceAccount.new(
506
+ token_url: data[:token_url] || 'https://example.com/token'
507
+ )
508
+ ensure
509
+ original_env ? ENV['RSPEC_ENV'] = original_env : ENV.delete('RSPEC_ENV')
510
+ end
511
+ when :google_service_account
512
+ original_env = ENV['RSPEC_ENV']
513
+ ENV['RSPEC_ENV'] = 'test'
514
+ begin
515
+ Legate::Auth::Schemes::GoogleServiceAccount.new(
516
+ scopes: data[:scopes] || ['https://www.googleapis.com/auth/cloud-platform']
517
+ )
518
+ ensure
519
+ original_env ? ENV['RSPEC_ENV'] = original_env : ENV.delete('RSPEC_ENV')
520
+ end
521
+ else
522
+ Legate.logger&.warn("Unknown scheme type: #{scheme_type}")
523
+ nil
524
+ end
525
+ rescue StandardError => e
526
+ Legate.logger&.warn("Failed to deserialize scheme: #{e.message}")
527
+ nil
528
+ end
529
+ end
530
+ end
531
+ end