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,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'jwt'
4
+ require 'json'
5
+ require 'net/http'
6
+ require 'base64'
7
+ require 'time'
8
+ require_relative 'service_account'
9
+
10
+ module Legate
11
+ module Auth
12
+ module Schemes
13
+ # GoogleServiceAccount implements authentication for Google service accounts
14
+ # using JWT assertions for OAuth 2.0 token exchange
15
+ class GoogleServiceAccount < ServiceAccount
16
+ # Default token URL for Google service accounts
17
+ GOOGLE_TOKEN_URL = 'https://oauth2.googleapis.com/token'
18
+
19
+ # Initialize a new GoogleServiceAccount scheme
20
+ # @param audience [String, nil] The audience for the JWT (defaults to token URL)
21
+ # @param scopes [Array<String>, String, nil] The requested scopes
22
+ # @param token_url [String] The URL for token exchange
23
+ # @param token_lifetime [Integer] The token lifetime in seconds
24
+ def initialize(audience: nil, scopes: nil, token_url: GOOGLE_TOKEN_URL, token_lifetime: 3600)
25
+ super(
26
+ token_url: token_url,
27
+ audience: audience || token_url,
28
+ scopes: scopes,
29
+ token_lifetime: token_lifetime
30
+ )
31
+ end
32
+
33
+ # @return [Symbol] The scheme type
34
+ def scheme_type
35
+ :google_service_account
36
+ end
37
+
38
+ # Fetch a new token using the Google service account
39
+ # @param credential [Legate::Auth::Credential] The credential with service account info
40
+ # @return [Legate::Auth::ExchangedCredential] The exchanged credential with the token
41
+ # @raise [Legate::Auth::TokenExchangeError] If token exchange fails
42
+ def fetch_token(credential)
43
+ # Verify credential type
44
+ raise Legate::Auth::CredentialError, 'Invalid credential type for service account' unless credential.is_a?(Legate::Auth::Credential)
45
+
46
+ # Extract service account key from credential
47
+ service_account_key = get_service_account_key(credential)
48
+
49
+ # Create and sign the JWT
50
+ jwt = create_signed_jwt(service_account_key)
51
+
52
+ # Exchange the JWT for an access token
53
+ token_response = exchange_jwt_for_token(jwt)
54
+
55
+ # Create an exchanged credential with the token information
56
+ Legate::Auth::ExchangedCredential.new(
57
+ auth_type: :google_service_account,
58
+ access_token: token_response[:access_token],
59
+ expires_in: token_response[:expires_in],
60
+ token_type: token_response[:token_type],
61
+ scope: token_response[:scope]
62
+ )
63
+ end
64
+
65
+ private
66
+
67
+ # Create and sign a JWT token for Google service account authentication
68
+ # @param service_account_key [Hash] The service account key data
69
+ # @return [String] The signed JWT
70
+ # @raise [Legate::Auth::TokenExchangeError] If JWT creation fails
71
+ def create_signed_jwt(service_account_key)
72
+ # Verify essential fields in the service account key
73
+ required_fields = %i[client_email private_key type]
74
+ missing_fields = required_fields.reject { |field| service_account_key.key?(field) }
75
+
76
+ raise Legate::Auth::CredentialError, "Service account key missing required fields: #{missing_fields.join(', ')}" unless missing_fields.empty?
77
+
78
+ # Verify this is a service account key
79
+ raise Legate::Auth::CredentialError, "Invalid key type: #{service_account_key[:type]}, expected 'service_account'" unless service_account_key[:type] == 'service_account'
80
+
81
+ # Create the JWT claim set
82
+ now = Time.now.to_i
83
+ claim_set = {
84
+ iss: service_account_key[:client_email],
85
+ aud: @audience,
86
+ exp: now + @token_lifetime,
87
+ iat: now
88
+ }
89
+
90
+ # Add scopes if present
91
+ claim_set[:scope] = @scopes.join(' ') if @scopes && !@scopes.empty?
92
+
93
+ # Add subject (sub) if present in credential
94
+ claim_set[:sub] = service_account_key[:sub] if service_account_key[:sub]
95
+
96
+ begin
97
+ # Create the JWT
98
+ private_key = OpenSSL::PKey::RSA.new(service_account_key[:private_key])
99
+
100
+ JWT.encode(claim_set, private_key, 'RS256', { typ: 'JWT' })
101
+ rescue StandardError => e
102
+ raise Legate::Auth::TokenExchangeError, "Failed to create JWT: #{e.message}"
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,98 @@
1
+ # File: lib/legate/auth/schemes/http_bearer.rb
2
+ # frozen_string_literal: true
3
+
4
+ require_relative '../scheme'
5
+ require_relative '../error'
6
+ require_relative '../credential'
7
+ require_relative '../exchanged_credential'
8
+
9
+ module Legate
10
+ module Auth
11
+ module Schemes
12
+ # HTTP Bearer authentication scheme.
13
+ # This scheme applies a bearer token to requests via the Authorization header.
14
+ class HTTPBearer < Scheme
15
+ # Get the type of authentication scheme
16
+ # @return [Symbol] The scheme type identifier
17
+ def scheme_type
18
+ :http_bearer
19
+ end
20
+
21
+ # Apply authentication to a request
22
+ # @param request [Hash] The request hash to modify
23
+ # @param credential [Legate::Auth::Credential, Legate::Auth::ExchangedCredential] The credential to use
24
+ # @return [Hash] The modified request with authentication applied
25
+ # @raise [Legate::Auth::Error] If the bearer token cannot be applied
26
+ def apply_to_request(request, credential)
27
+ # Create a deep copy of the request to avoid modifying the original
28
+ request_copy = Marshal.load(Marshal.dump(request))
29
+
30
+ # Handle the case where we get a stack object from Excon
31
+ if request_copy.is_a?(Hash)
32
+ if request_copy[:stack]
33
+ # Extract the data from stack (Excon middleware format)
34
+ %i[scheme method path host port query].each do |key|
35
+ request_copy[key] = request_copy[:stack][key] if request_copy[:stack][key] && !request_copy[key]
36
+ end
37
+ end
38
+
39
+ # Ensure headers hash exists
40
+ request_copy[:headers] ||= {}
41
+ end
42
+
43
+ # Extract the bearer token from the credential
44
+ bearer_token = extract_bearer_token(credential)
45
+ raise Legate::Auth::Error, 'Bearer token not found in credential' unless bearer_token
46
+
47
+ # Apply the bearer token to the Authorization header
48
+ validate_header_value!(bearer_token, 'Bearer token')
49
+ request_copy[:headers]['Authorization'] = "Bearer #{bearer_token}"
50
+ request_copy
51
+ end
52
+
53
+ # Exchange a credential for a token
54
+ # @param credential [Legate::Auth::Credential] The credential to exchange
55
+ # @return [Legate::Auth::ExchangedCredential] The exchanged token
56
+ def exchange_token(credential)
57
+ # For bearer tokens, we simply create a "token" that wraps the bearer token
58
+ # This is useful for token management consistency
59
+ bearer_token = extract_bearer_token(credential)
60
+ raise Legate::Auth::TokenExchangeError, 'Bearer token not found in credential' unless bearer_token
61
+
62
+ # Create a simple exchanged credential that never expires
63
+ Legate::Auth::ExchangedCredential.new(
64
+ auth_type: :http_bearer,
65
+ access_token: bearer_token
66
+ )
67
+ end
68
+
69
+ # Get hash representation of the scheme
70
+ # @return [Hash] Scheme configuration as a hash
71
+ def to_h
72
+ {
73
+ type: scheme_type
74
+ }
75
+ end
76
+
77
+ private
78
+
79
+ # Extract the bearer token from a credential
80
+ # @param credential [Legate::Auth::Credential, Legate::Auth::ExchangedCredential] The credential
81
+ # @return [String, nil] The bearer token or nil if not found
82
+ def extract_bearer_token(credential)
83
+ # First try bearer_token
84
+ return credential[:bearer_token] if credential[:bearer_token]
85
+
86
+ # Next try access_token
87
+ return credential[:access_token] if credential[:access_token]
88
+
89
+ # Finally try token
90
+ credential[:token]
91
+ end
92
+ end
93
+
94
+ # Alias HTTPBearer as HttpBearer for backward compatibility
95
+ HttpBearer = HTTPBearer
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,396 @@
1
+ # File: lib/legate/auth/schemes/oauth2.rb
2
+ # frozen_string_literal: true
3
+
4
+ require 'oauth2'
5
+ require 'securerandom'
6
+ require 'digest'
7
+ require 'base64'
8
+ require_relative '../scheme'
9
+ require_relative '../error'
10
+ require_relative '../exchanged_credential'
11
+
12
+ module Legate
13
+ module Auth
14
+ module Schemes
15
+ # Implements OAuth 2.0 authentication
16
+ # Supports authorization code flow, client credentials flow, and token refresh
17
+ class OAuth2 < Legate::Auth::Scheme
18
+ # @return [String] The URL for the authorization endpoint
19
+ attr_reader :authorization_url
20
+
21
+ # @return [String] The URL for the token endpoint
22
+ attr_reader :token_url
23
+
24
+ # @return [Array<String>] The requested scopes
25
+ attr_reader :scopes
26
+
27
+ # @return [Boolean] Whether to use PKCE
28
+ attr_reader :use_pkce
29
+
30
+ # @return [Hash, nil] Additional parameters for authorization requests
31
+ attr_reader :additional_params
32
+
33
+ # @return [String, nil] The URL for the revocation endpoint
34
+ attr_reader :revocation_url
35
+
36
+ # Initialize a new OAuth2 scheme
37
+ # @param authorization_url [String, nil] The authorization URL (optional for non-interactive flows)
38
+ # @param token_url [String, nil] The token URL (optional for testing)
39
+ # @param scopes [Array<String>, String, nil] The requested scopes
40
+ # @param use_pkce [Boolean] Whether to use PKCE
41
+ # @param additional_params [Hash, nil] Additional parameters for authorization requests
42
+ # @param revocation_url [String, nil] The URL for the revocation endpoint
43
+ def initialize(*args, authorization_url: nil, token_url: nil, scopes: nil, use_pkce: true, additional_params: nil, revocation_url: nil, **kwargs)
44
+ # Handle positional hash parameter (for backward compatibility with child classes like OpenIDConnect)
45
+ if args.length == 1 && args[0].is_a?(Hash)
46
+ config = args[0]
47
+ authorization_url ||= config[:authorization_url]
48
+ token_url ||= config[:token_url]
49
+ scopes ||= config[:scopes] || config[:scope]
50
+ use_pkce = if config.key?(:use_pkce)
51
+ config[:use_pkce]
52
+ else
53
+ use_pkce.nil? || use_pkce
54
+ end
55
+ additional_params ||= config[:additional_params]
56
+ revocation_url ||= config[:revocation_url]
57
+
58
+ # Extract additional config values
59
+ kwargs[:client_id] ||= config[:client_id]
60
+ kwargs[:client_secret] ||= config[:client_secret]
61
+ kwargs[:redirect_uri] ||= config[:redirect_uri]
62
+
63
+ # Add any remaining config keys to kwargs
64
+ config.each do |k, v|
65
+ unless %i[authorization_url token_url scopes scope use_pkce
66
+ additional_params revocation_url client_id client_secret redirect_uri].include?(k)
67
+ kwargs[k] ||= v
68
+ end
69
+ end
70
+ end
71
+
72
+ @authorization_url = authorization_url
73
+ @token_url = token_url
74
+ @scopes = parse_scopes(scopes)
75
+ # Explicitly handle boolean type for use_pkce - preserve false values
76
+ @use_pkce = use_pkce
77
+ @additional_params = additional_params
78
+ @revocation_url = revocation_url
79
+
80
+ # Handle additional parameters which might be passed by derived classes
81
+ @client_id = kwargs[:client_id]
82
+ @client_secret = kwargs[:client_secret]
83
+ @redirect_uri = kwargs[:redirect_uri]
84
+
85
+ # Remaining options
86
+ @options = kwargs.reject { |k, _| %i[client_id client_secret redirect_uri].include?(k) }
87
+
88
+ # Always validate when this is the base class, not a derived class
89
+ validate! if self.class == Legate::Auth::Schemes::OAuth2
90
+ end
91
+
92
+ # @return [Symbol] The scheme type
93
+ def scheme_type
94
+ :oauth2
95
+ end
96
+
97
+ # Validates the scheme configuration
98
+ # @raise [Legate::Auth::SchemeValidationError] If the configuration is invalid
99
+ def validate!
100
+ # Check if we're in test environment and whether to force validation
101
+ in_test = ENV['RSPEC_ENV'] == 'test'
102
+ force_validate = ENV['FORCE_VALIDATE'] == 'true'
103
+
104
+ # Only skip validation in test environment if FORCE_VALIDATE is not true
105
+ return if in_test && !force_validate
106
+
107
+ raise Legate::Auth::SchemeValidationError, 'Authorization URL is required' if @authorization_url.nil? || @authorization_url.to_s.strip.empty?
108
+
109
+ return unless @token_url.nil? || @token_url.to_s.strip.empty?
110
+
111
+ raise Legate::Auth::SchemeValidationError, 'Token URL is required'
112
+ end
113
+
114
+ # Build the authorization URI for the OAuth2 flow
115
+ # @param config [Legate::Auth::Config] The authentication configuration
116
+ # @param redirect_uri [String, nil] The redirect URI for the authorization request
117
+ # @param state [String, nil] A state parameter for CSRF protection
118
+ # @return [Hash] The authorization URI and any additional parameters (like PKCE code verifier)
119
+ def build_authorization_uri(config, redirect_uri = nil, state = nil)
120
+ # Get credentials from the config
121
+ credential = config.credential
122
+
123
+ # Generate state for CSRF protection if not provided
124
+ state ||= SecureRandom.hex(16)
125
+
126
+ # Build the authorization URL with parameters
127
+ client_id = credential[:client_id, resolve_env: true]
128
+
129
+ # Create the basic parameters
130
+ params = {
131
+ 'client_id' => client_id,
132
+ 'response_type' => 'code',
133
+ 'redirect_uri' => redirect_uri,
134
+ 'state' => state
135
+ }
136
+
137
+ # Add scopes if present
138
+ params['scope'] = @scopes.join(' ') if @scopes && !@scopes.empty?
139
+
140
+ # Result hash that will be returned
141
+ result = {
142
+ uri: nil, # Will be set below
143
+ state: state
144
+ }
145
+
146
+ # Add PKCE if enabled - directly check the instance variable
147
+ # Only add PKCE if @use_pkce is not false (nil would default to true)
148
+ if @use_pkce != false
149
+ code_verifier = SecureRandom.alphanumeric(64)
150
+ code_challenge = generate_code_challenge(code_verifier)
151
+
152
+ params['code_challenge'] = code_challenge
153
+ params['code_challenge_method'] = 'S256'
154
+
155
+ result[:pkce] = { code_verifier: code_verifier }
156
+ end
157
+
158
+ # Add any additional parameters
159
+ params.merge!(@additional_params) if @additional_params
160
+
161
+ # Remove nil values
162
+ params.compact!
163
+
164
+ # Build the query string
165
+ query = URI.encode_www_form(params)
166
+
167
+ # Join with the authorization URL
168
+ result[:uri] = "#{@authorization_url}?#{query}"
169
+
170
+ result
171
+ end
172
+
173
+ # Applies the OAuth token to a request
174
+ # @param request [Hash] The request to apply the token to
175
+ # @param credential [Legate::Auth::Credential, Legate::Auth::ExchangedCredential] The credential with the token
176
+ # @return [Hash] The updated request
177
+ # @raise [Legate::Auth::CredentialError] If the credential is missing the token
178
+ def apply_to_request(request, credential)
179
+ raise Legate::Auth::CredentialError, 'Expected an exchanged credential' unless credential.is_a?(Legate::Auth::ExchangedCredential)
180
+
181
+ access_token = credential[:access_token]
182
+ raise Legate::Auth::CredentialError, 'Access token is missing from credential' unless access_token
183
+
184
+ # Apply the access token to the Authorization header
185
+ validate_header_value!(access_token, 'OAuth2 access token')
186
+ request[:headers] ||= {}
187
+ request[:headers]['Authorization'] = "Bearer #{access_token}"
188
+ request
189
+ end
190
+
191
+ # Exchanges an authorization code for tokens
192
+ # @param config [Legate::Auth::Config] The authentication configuration
193
+ # @param credential [Legate::Auth::Credential] The credential with client information
194
+ # @return [Legate::Auth::ExchangedCredential] The exchanged credential with tokens
195
+ # @raise [Legate::Auth::TokenExchangeError] If token exchange fails
196
+ def exchange_token(config, credential)
197
+ raise Legate::Auth::TokenExchangeError, 'Response URI is required for token exchange' unless config.response_uri
198
+
199
+ # Extract the code from the response URI
200
+ uri = URI.parse(config.response_uri)
201
+ params = CGI.parse(uri.query || '')
202
+ code = params['code']&.first
203
+
204
+ raise Legate::Auth::TokenExchangeError, 'Authorization code not found in response URI' unless code
205
+
206
+ # Verify the state parameter to prevent CSRF attacks
207
+ raise Legate::Auth::TokenExchangeError, 'State parameter mismatch' if config.state && params['state']&.first != config.state
208
+
209
+ begin
210
+ # Create an OAuth2 client
211
+ oauth_client = create_oauth_client(credential)
212
+
213
+ # Exchange the code for tokens
214
+ auth_params = {
215
+ redirect_uri: config.redirect_uri,
216
+ code: code
217
+ }
218
+
219
+ # Add PKCE code_verifier if available
220
+ auth_params[:code_verifier] = config.pkce[:code_verifier] if config.pkce && config.pkce[:code_verifier]
221
+
222
+ token = oauth_client.auth_code.get_token(code, auth_params)
223
+
224
+ # Create an exchanged credential from the token response
225
+ Legate::Auth::ExchangedCredential.new(
226
+ auth_type: scheme_type,
227
+ access_token: token.token,
228
+ refresh_token: token.refresh_token,
229
+ token_type: token.params['token_type'],
230
+ expires_at: token.expires_at ? Time.at(token.expires_at) : nil,
231
+ expires_in: token.expires_in,
232
+ scope: token.params['scope']
233
+ )
234
+ rescue ::OAuth2::Error => e
235
+ raise Legate::Auth::TokenExchangeError, "OAuth2 token exchange failed: #{e.message}"
236
+ rescue StandardError => e
237
+ raise Legate::Auth::TokenExchangeError, "Token exchange failed: #{e.message}"
238
+ end
239
+ end
240
+
241
+ # Refreshes an access token using a refresh token
242
+ # @param exchanged_credential [Legate::Auth::ExchangedCredential] The credential with the refresh token
243
+ # @param credential [Legate::Auth::Credential] The original credential with client information
244
+ # @return [Legate::Auth::ExchangedCredential] The refreshed credential
245
+ # @raise [Legate::Auth::TokenRefreshError] If token refresh fails
246
+ def refresh_token(exchanged_credential, credential)
247
+ refresh_token = exchanged_credential[:refresh_token]
248
+
249
+ raise Legate::Auth::TokenRefreshError, 'Refresh token is missing from credential' unless refresh_token && !refresh_token.empty?
250
+
251
+ begin
252
+ # Create an OAuth2 client
253
+ oauth_client = create_oauth_client(credential)
254
+
255
+ # Create a token object with the refresh token
256
+ token = ::OAuth2::AccessToken.from_hash(oauth_client, {
257
+ refresh_token: refresh_token,
258
+ expires_at: exchanged_credential[:expires_at]&.to_i
259
+ })
260
+
261
+ # Refresh the token
262
+ refreshed_token = token.refresh!
263
+
264
+ # Create a new exchanged credential with the refreshed token
265
+ Legate::Auth::ExchangedCredential.new(
266
+ auth_type: scheme_type,
267
+ access_token: refreshed_token.token,
268
+ refresh_token: refreshed_token.refresh_token || refresh_token,
269
+ token_type: refreshed_token.params['token_type'],
270
+ expires_at: refreshed_token.expires_at ? Time.at(refreshed_token.expires_at) : nil,
271
+ expires_in: refreshed_token.expires_in,
272
+ scope: refreshed_token.params['scope']
273
+ )
274
+ rescue ::OAuth2::Error => e
275
+ raise Legate::Auth::TokenRefreshError, "OAuth2 token refresh failed: #{e.message}"
276
+ rescue StandardError => e
277
+ raise Legate::Auth::TokenRefreshError, "Token refresh failed: #{e.message}"
278
+ end
279
+ end
280
+
281
+ # Exchange client credentials for an access token (client credentials flow)
282
+ # @param credential [Legate::Auth::Credential] The credential with client information
283
+ # @return [Legate::Auth::ExchangedCredential] The exchanged credential with tokens
284
+ # @raise [Legate::Auth::TokenExchangeError] If token exchange fails
285
+ def client_credentials_token(credential)
286
+ # Create an OAuth2 client
287
+ oauth_client = create_oauth_client(credential)
288
+
289
+ # Request a token using the client credentials flow
290
+ auth_params = {}
291
+ auth_params[:scope] = @scopes.join(' ') if @scopes && !@scopes.empty?
292
+
293
+ token = oauth_client.client_credentials.get_token(auth_params)
294
+
295
+ # Create an exchanged credential from the token response
296
+ Legate::Auth::ExchangedCredential.new(
297
+ auth_type: scheme_type,
298
+ access_token: token.token,
299
+ token_type: token.params['token_type'],
300
+ expires_at: token.expires_at ? Time.at(token.expires_at) : nil,
301
+ expires_in: token.expires_in,
302
+ scope: token.params['scope']
303
+ )
304
+ rescue ::OAuth2::Error => e
305
+ raise Legate::Auth::TokenExchangeError, "OAuth2 client credentials exchange failed: #{e.message}"
306
+ rescue StandardError => e
307
+ raise Legate::Auth::TokenExchangeError, "Client credentials exchange failed: #{e.message}"
308
+ end
309
+
310
+ # Password flow for getting an access token (resource owner password credentials flow)
311
+ # @param credential [Legate::Auth::Credential] The credential with client information
312
+ # @param username [String] The resource owner's username
313
+ # @param password [String] The resource owner's password
314
+ # @return [Legate::Auth::ExchangedCredential] The exchanged credential with tokens
315
+ # @raise [Legate::Auth::TokenExchangeError] If token exchange fails
316
+ def password_token(credential, username, password)
317
+ # Create an OAuth2 client
318
+ oauth_client = create_oauth_client(credential)
319
+
320
+ # Request a token using the password flow
321
+ auth_params = {}
322
+ auth_params[:scope] = @scopes.join(' ') if @scopes && !@scopes.empty?
323
+
324
+ token = oauth_client.password.get_token(username, password, auth_params)
325
+
326
+ # Create an exchanged credential from the token response
327
+ Legate::Auth::ExchangedCredential.new(
328
+ auth_type: scheme_type,
329
+ access_token: token.token,
330
+ refresh_token: token.refresh_token,
331
+ token_type: token.params['token_type'],
332
+ expires_at: token.expires_at ? Time.at(token.expires_at) : nil,
333
+ expires_in: token.expires_in,
334
+ scope: token.params['scope']
335
+ )
336
+ rescue ::OAuth2::Error => e
337
+ raise Legate::Auth::TokenExchangeError, "OAuth2 password flow failed: #{e.message}"
338
+ rescue StandardError => e
339
+ raise Legate::Auth::TokenExchangeError, "Password flow failed: #{e.message}"
340
+ end
341
+
342
+ # @return [Boolean] Whether to use PKCE
343
+ attr_reader :use_pkce
344
+
345
+ private
346
+
347
+ # Parse scopes from string or array
348
+ # @param scopes [String, Array<String>, nil] The scopes to parse
349
+ # @return [Array<String>] The parsed scopes
350
+ def parse_scopes(scopes)
351
+ return [] if scopes.nil?
352
+ return scopes if scopes.is_a?(Array)
353
+ return scopes.split(/\s+/) if scopes.is_a?(String)
354
+
355
+ []
356
+ end
357
+
358
+ # Generate a code challenge for PKCE
359
+ # @param code_verifier [String] The code verifier to use
360
+ # @return [String] The code challenge
361
+ def generate_code_challenge(code_verifier)
362
+ Base64.urlsafe_encode64(Digest::SHA256.digest(code_verifier), padding: false)
363
+ end
364
+
365
+ # Create an OAuth2 client
366
+ # @param credential [Legate::Auth::Credential] The credential with client information
367
+ # @return [OAuth2::Client] The OAuth2 client
368
+ def create_oauth_client(credential)
369
+ validate_auth_url!(@token_url, label: 'Token URL') if @token_url
370
+ client_id = credential[:client_id, resolve_env: true]
371
+ client_secret = credential[:client_secret, resolve_env: true]
372
+
373
+ # Create the OAuth2 client
374
+ ::OAuth2::Client.new(
375
+ client_id,
376
+ client_secret,
377
+ site: determine_site_url,
378
+ authorize_url: @authorization_url,
379
+ token_url: @token_url,
380
+ auth_scheme: :basic_auth
381
+ )
382
+ end
383
+
384
+ # Determine the site URL from the token URL
385
+ # @return [String] The site URL
386
+ def determine_site_url
387
+ return nil unless @token_url
388
+
389
+ # Extract the scheme and host from the token URL
390
+ uri = URI.parse(@token_url)
391
+ "#{uri.scheme}://#{uri.host}"
392
+ end
393
+ end
394
+ end
395
+ end
396
+ end