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,362 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'token_store'
4
+ require_relative 'exchanged_credential'
5
+ require_relative 'error'
6
+
7
+ module Legate
8
+ module Auth
9
+ # TokenManager is responsible for managing the lifecycle of authentication tokens.
10
+ # It provides a centralized system for token acquisition, refresh, and invalidation.
11
+ # This class works with the TokenStore for persistence and the various authentication
12
+ # schemes for token operations.
13
+ class TokenManager
14
+ # Default configuration values
15
+ DEFAULT_CONFIG = {
16
+ refresh_buffer: 60, # Seconds before expiration to trigger refresh
17
+ retry_max_attempts: 3, # Maximum number of refresh retry attempts
18
+ retry_delay: 2, # Initial delay between retries (seconds)
19
+ retry_backoff: 1.5, # Backoff multiplier for subsequent retries
20
+ auto_refresh: true, # Whether to automatically refresh tokens
21
+ background_refresh: false # Whether to refresh tokens in background
22
+ }.freeze
23
+
24
+ # Initialize a new TokenManager
25
+ # @param token_store [Legate::Auth::TokenStore] The token store for persistence
26
+ # @param config [Hash] Configuration options
27
+ def initialize(token_store, config = {})
28
+ @token_store = token_store
29
+ @config = DEFAULT_CONFIG.merge(config)
30
+ @callbacks = {
31
+ before_expiry: [],
32
+ refresh_success: [],
33
+ refresh_failure: [],
34
+ invalidated: []
35
+ }
36
+ @lock = Mutex.new
37
+ end
38
+
39
+ # Get a token for the given scheme and credential
40
+ # @param scheme [Legate::Auth::Scheme] The authentication scheme
41
+ # @param credential [Legate::Auth::Credential] The credential
42
+ # @param force_refresh [Boolean] Whether to force a token refresh
43
+ # @return [Legate::Auth::ExchangedCredential, nil] The token or nil if not available
44
+ def get_token(scheme, credential, force_refresh: false)
45
+ raise ArgumentError, 'Scheme must be an Legate::Auth::Scheme' unless scheme.is_a?(Legate::Auth::Scheme)
46
+
47
+ cache_key = generate_cache_key(scheme, credential)
48
+
49
+ # Use a mutex to prevent race conditions during token retrieval/refresh
50
+ @lock.synchronize do
51
+ # Try to get the token from the store
52
+ token = @token_store.get(cache_key)
53
+
54
+ # If no token, refresh it
55
+ return refresh_token(scheme, credential, nil, cache_key) if token.nil?
56
+
57
+ # Check if a scheme supports refresh
58
+ supports_refresh = scheme.respond_to?(:supports_refresh?) && scheme.supports_refresh?
59
+ is_refreshable = token.respond_to?(:refreshable?) && token.refreshable?
60
+
61
+ # If force refresh is requested, refresh the token regardless of expiration
62
+ if force_refresh
63
+ # Check if the token is refreshable before attempting to refresh
64
+ return refresh_token(scheme, credential, token, cache_key) if supports_refresh && is_refreshable
65
+
66
+ # For tokens that aren't refreshable, create a new one
67
+ invalidate_token(cache_key)
68
+ new_token = exchange_token(scheme, credential)
69
+ if new_token
70
+ @token_store.store(cache_key, new_token)
71
+ trigger_callback(:refresh_success, new_token, scheme, credential)
72
+ return new_token
73
+ end
74
+
75
+ end
76
+
77
+ # Check if token needs refresh based on expiration
78
+ if needs_refresh?(token)
79
+ # Only try to refresh if the scheme supports it and the token is refreshable
80
+ return refresh_token(scheme, credential, token, cache_key) if supports_refresh && is_refreshable
81
+
82
+ # If not refreshable, invalidate and return nil
83
+ invalidate_token(cache_key)
84
+ return nil
85
+
86
+ end
87
+
88
+ # Check if token is approaching expiration and trigger callback
89
+ if approaching_expiration?(token)
90
+ trigger_callback(:before_expiry, token, scheme, credential)
91
+
92
+ # If auto_refresh is enabled and we can refresh, do it
93
+ return refresh_token(scheme, credential, token, cache_key) if @config[:auto_refresh] && supports_refresh && is_refreshable
94
+ end
95
+
96
+ token
97
+ end
98
+ end
99
+
100
+ # Explicitly refresh a token
101
+ # @param scheme [Legate::Auth::Scheme] The authentication scheme
102
+ # @param credential [Legate::Auth::Credential] The credential
103
+ # @param token [Legate::Auth::ExchangedCredential, nil] The current token, if available
104
+ # @return [Legate::Auth::ExchangedCredential, nil] The refreshed token or nil on failure
105
+ def refresh_token(scheme, credential, token = nil, cache_key = nil)
106
+ raise ArgumentError, 'Scheme must be an Legate::Auth::Scheme' unless scheme.is_a?(Legate::Auth::Scheme)
107
+
108
+ cache_key ||= generate_cache_key(scheme, credential)
109
+
110
+ # If we don't have a token and it's an oauth or service account scheme,
111
+ # we need to authenticate from scratch
112
+ if token.nil?
113
+ if %i[oauth2 oidc service_account].include?(scheme.scheme_type)
114
+ # For these schemes, we need a complete authentication flow
115
+ # which can't be handled here - return nil to indicate need for full auth
116
+ return nil
117
+ end
118
+
119
+ # For other schemes, we can simply apply the credential
120
+ begin
121
+ # Basic auth, API key, etc. - create a new token directly
122
+ token = exchange_token(scheme, credential)
123
+ if token
124
+ @token_store.store(cache_key, token)
125
+ trigger_callback(:refresh_success, token, scheme, credential)
126
+ end
127
+ return token
128
+ rescue Legate::Auth::Error => e
129
+ Legate.logger.error("Failed to create token: #{e.message}")
130
+ trigger_callback(:refresh_failure, nil, scheme, credential, error: e)
131
+ return nil
132
+ end
133
+ end
134
+
135
+ # Token exists - attempt to refresh it if scheme supports refresh
136
+ supports_refresh = scheme.respond_to?(:supports_refresh?) && scheme.supports_refresh?
137
+ is_refreshable = token.respond_to?(:refreshable?) && token.refreshable?
138
+
139
+ if supports_refresh && is_refreshable
140
+ begin
141
+ refreshed = scheme.refresh_token(token, credential)
142
+ if refreshed
143
+ @token_store.store(cache_key, refreshed)
144
+ trigger_callback(:refresh_success, refreshed, scheme, credential)
145
+ return refreshed
146
+ else
147
+ # Handle the case where refresh_token returns nil but doesn't raise an error
148
+ Legate.logger.error('Failed to refresh token: refresh_token returned nil')
149
+ trigger_callback(:refresh_failure, token, scheme, credential, error: nil)
150
+ return nil
151
+ end
152
+ rescue Legate::Auth::TokenRefreshError => e
153
+ Legate.logger.error("Failed to refresh token: #{e.message}")
154
+ trigger_callback(:refresh_failure, token, scheme, credential, error: e)
155
+ return nil
156
+ end
157
+ end
158
+
159
+ # Scheme doesn't support refresh or token isn't refreshable
160
+ # Return existing token if it's not expired
161
+ return token unless token.expired?
162
+
163
+ # Otherwise, invalidate it
164
+ invalidate_token(cache_key)
165
+ nil
166
+ end
167
+
168
+ # Invalidate a token, removing it from the store
169
+ # @param cache_key [String] The cache key for the token
170
+ # @return [Boolean] True if the token was invalidated
171
+ def invalidate_token(cache_key)
172
+ result = @token_store.clear(cache_key)
173
+ trigger_callback(:invalidated, nil, nil, nil, cache_key: cache_key) if result
174
+ result
175
+ end
176
+
177
+ # Revoke a token with the authentication provider
178
+ # @param scheme [Legate::Auth::Scheme] The authentication scheme
179
+ # @param credential [Legate::Auth::Credential] The credential
180
+ # @param token [Legate::Auth::ExchangedCredential] The token to revoke
181
+ # @return [Boolean] True if the token was revoked
182
+ def revoke_token(scheme, credential, token)
183
+ raise ArgumentError, 'Scheme must be an Legate::Auth::Scheme' unless scheme.is_a?(Legate::Auth::Scheme)
184
+ raise ArgumentError, 'Token must be an ExchangedCredential' unless token.is_a?(Legate::Auth::ExchangedCredential)
185
+
186
+ # Check if scheme supports revocation
187
+ unless scheme.respond_to?(:revoke_token)
188
+ Legate.logger.warn("Scheme #{scheme.scheme_type} does not support token revocation")
189
+ return false
190
+ end
191
+
192
+ begin
193
+ # Attempt to revoke the token
194
+ result = scheme.revoke_token(token, credential)
195
+
196
+ # Invalidate the token in our store if revocation succeeded
197
+ if result
198
+ cache_key = generate_cache_key(scheme, credential)
199
+ invalidate_token(cache_key)
200
+ end
201
+
202
+ result
203
+ rescue Legate::Auth::Error => e
204
+ Legate.logger.error("Failed to revoke token: #{e.message}")
205
+ false
206
+ rescue NotImplementedError => e
207
+ Legate.logger.error("#{e.message}")
208
+ false
209
+ end
210
+ end
211
+
212
+ # Register a callback for token lifecycle events
213
+ # @param event [Symbol] The event to register for (:before_expiry, :refresh_success, :refresh_failure, :invalidated)
214
+ # @param callback [Proc] The callback to execute
215
+ # @return [self]
216
+ def on(event, &callback)
217
+ raise ArgumentError, "Unknown event: #{event}. Valid events: #{@callbacks.keys.join(', ')}" unless @callbacks.key?(event)
218
+
219
+ @callbacks[event] << callback
220
+ self
221
+ end
222
+
223
+ private
224
+
225
+ # Check if a token needs to be refreshed
226
+ # @param token [Legate::Auth::ExchangedCredential] The token to check
227
+ # @return [Boolean] True if the token needs to be refreshed
228
+ def needs_refresh?(token)
229
+ token.expired?(@config[:refresh_buffer])
230
+ end
231
+
232
+ # Check if a token is approaching expiration
233
+ # @param token [Legate::Auth::ExchangedCredential] The token to check
234
+ # @return [Boolean] True if the token is approaching expiration
235
+ def approaching_expiration?(token)
236
+ return false unless token.expires_at
237
+
238
+ # Consider a token approaching expiration if it's within 2x the refresh buffer
239
+ buffer = @config[:refresh_buffer] * 2
240
+ (token.expires_at - Time.now) <= buffer
241
+ end
242
+
243
+ # Generate a cache key for the token
244
+ # @param scheme [Legate::Auth::Scheme] The authentication scheme
245
+ # @param credential [Legate::Auth::Credential] The credential
246
+ # @return [String] The cache key
247
+ def generate_cache_key(scheme, credential)
248
+ require 'digest/sha2'
249
+
250
+ # Create a unique key based on scheme and credential
251
+ parts = [
252
+ scheme.scheme_type.to_s,
253
+ credential.auth_type.to_s
254
+ ]
255
+
256
+ # Add scheme-specific information
257
+ case scheme.scheme_type
258
+ when :api_key
259
+ parts << credential[:api_key, resolve_env: false].to_s
260
+ when :http_bearer
261
+ parts << credential[:bearer_token, resolve_env: false].to_s
262
+ when :oauth2, :oidc
263
+ parts << credential[:client_id, resolve_env: false].to_s
264
+ parts << (credential[:scope, resolve_env: false] || '').to_s
265
+ when :service_account
266
+ parts << credential[:client_email, resolve_env: false].to_s
267
+ end
268
+
269
+ "auth_#{Digest::SHA256.hexdigest(parts.join(':'))}"
270
+ end
271
+
272
+ # Perform token refresh with retry logic
273
+ # @param scheme [Legate::Auth::Scheme] The authentication scheme
274
+ # @param credential [Legate::Auth::Credential] The credential
275
+ # @param token [Legate::Auth::ExchangedCredential] The current token
276
+ # @param cache_key [String] The cache key
277
+ # @return [Legate::Auth::ExchangedCredential, nil] The refreshed token or nil on failure
278
+ def perform_token_refresh(scheme, credential, token, cache_key)
279
+ attempts = 0
280
+ delay = @config[:retry_delay]
281
+
282
+ loop do
283
+ refreshed = scheme.refresh_token(token, credential)
284
+ if refreshed
285
+ @token_store.store(cache_key, refreshed)
286
+ trigger_callback(:refresh_success, refreshed, scheme, credential)
287
+ return refreshed
288
+ else
289
+ # Handle the case where refresh_token returns nil but doesn't raise an error
290
+ Legate.logger.error('Failed to refresh token: refresh_token returned nil')
291
+ trigger_callback(:refresh_failure, token, scheme, credential, error: nil)
292
+ return nil
293
+ end
294
+ rescue Legate::Auth::TokenRefreshError => e
295
+ attempts += 1
296
+
297
+ # Check if we've exceeded max attempts
298
+ if attempts >= @config[:retry_max_attempts]
299
+ Legate.logger.error("Failed to refresh token after #{attempts} attempts: #{e.message}")
300
+ trigger_callback(:refresh_failure, token, scheme, credential, error: e)
301
+ return nil
302
+ end
303
+
304
+ # Exponential backoff for retry
305
+ sleep delay
306
+ delay *= @config[:retry_backoff]
307
+ end
308
+ end
309
+
310
+ # Exchange a credential for a token
311
+ # @param scheme [Legate::Auth::Scheme] The authentication scheme
312
+ # @param credential [Legate::Auth::Credential] The credential
313
+ # @return [Legate::Auth::ExchangedCredential, nil] The exchanged token or nil on failure
314
+ def exchange_token(scheme, credential)
315
+ case scheme.scheme_type
316
+ when :api_key
317
+ # Create a simple exchanged credential for API key
318
+ Legate::Auth::ExchangedCredential.new(
319
+ auth_type: :api_key,
320
+ access_token: credential[:api_key],
321
+ token_type: 'ApiKey'
322
+ )
323
+ when :http_bearer
324
+ # Create a simple exchanged credential for Bearer token
325
+ Legate::Auth::ExchangedCredential.new(
326
+ auth_type: :http_bearer,
327
+ access_token: credential[:bearer_token],
328
+ token_type: 'Bearer'
329
+ )
330
+ else
331
+ # Other types like OAuth2 require a more complex flow
332
+ # and cannot be handled directly here
333
+ nil
334
+ end
335
+ end
336
+
337
+ # Trigger a callback for the given event
338
+ # @param event [Symbol] The event that occurred
339
+ # @param token [Legate::Auth::ExchangedCredential, nil] The token involved in the event
340
+ # @param scheme [Legate::Auth::Scheme, nil] The authentication scheme
341
+ # @param credential [Legate::Auth::Credential, nil] The credential
342
+ # @param extras [Hash] Extra information to pass to the callback
343
+ def trigger_callback(event, token, scheme, credential, extras = {})
344
+ return unless @callbacks.key?(event)
345
+
346
+ @callbacks[event].each do |callback|
347
+ # Create a hash with all the data
348
+ data = {
349
+ event: event,
350
+ token: token,
351
+ scheme: scheme,
352
+ credential: credential
353
+ }.merge(extras)
354
+
355
+ callback.call(data)
356
+ rescue StandardError => e
357
+ Legate.logger.error("Error in #{event} callback: #{e.message}")
358
+ end
359
+ end
360
+ end
361
+ end
362
+ end
@@ -0,0 +1,86 @@
1
+ # File: lib/legate/auth/token_store.rb
2
+ # frozen_string_literal: true
3
+
4
+ require_relative 'exchanged_credential'
5
+
6
+ module Legate
7
+ module Auth
8
+ # Provides a token store for caching authentication tokens
9
+ # This class wraps a session service and provides methods
10
+ # for storing, retrieving, and clearing tokens
11
+ class TokenStore
12
+ # Initialize a new token store
13
+ # @param session_service [Legate::SessionService::Base] The session service to use
14
+ def initialize(session_service)
15
+ @session_service = session_service
16
+ @scope = 'auth' # Scoped state namespace for authentication
17
+ end
18
+
19
+ # Store a token in the cache
20
+ # @param key [String] The cache key
21
+ # @param token [Legate::Auth::ExchangedCredential] The token to store
22
+ # @return [Boolean] True if the token was stored
23
+ def store(key, token)
24
+ return false unless token.is_a?(Legate::Auth::ExchangedCredential)
25
+
26
+ begin
27
+ # Serialize token to hash
28
+ token_data = token.to_h
29
+
30
+ # Store in scoped state
31
+ @session_service.save_scoped_state(@scope, key, token_data)
32
+ true
33
+ rescue StandardError => e
34
+ Legate.logger.error("Failed to store token: #{e.message}")
35
+ false
36
+ end
37
+ end
38
+
39
+ # Get a token from the cache
40
+ # @param key [String] The cache key
41
+ # @return [Legate::Auth::ExchangedCredential, nil] The token or nil if not found or expired
42
+ def get(key)
43
+ # Retrieve from scoped state
44
+ token_data = @session_service.load_scoped_state(@scope, key)
45
+ return nil unless token_data
46
+
47
+ # Deserialize to token object
48
+ token = Legate::Auth::ExchangedCredential.from_h(token_data)
49
+
50
+ # Check expiration
51
+ if token.expired?
52
+ Legate.logger.debug("Retrieved expired token from cache (key: #{key})")
53
+ # Clear expired token
54
+ clear(key)
55
+ return nil
56
+ end
57
+
58
+ token
59
+ rescue StandardError => e
60
+ Legate.logger.error("Failed to retrieve token: #{e.message}")
61
+ nil
62
+ end
63
+
64
+ # Clear a token from the cache
65
+ # @param key [String] The cache key to clear
66
+ # @return [Boolean] True if the token was cleared
67
+ def clear(key)
68
+ @session_service.clear_scoped_state(@scope, key)
69
+ true
70
+ rescue StandardError => e
71
+ Legate.logger.error("Failed to clear token: #{e.message}")
72
+ false
73
+ end
74
+
75
+ # Clear all tokens from the cache
76
+ # @return [Boolean] True if all tokens were cleared
77
+ def clear_all
78
+ @session_service.clear_scoped_state(@scope, '*')
79
+ true
80
+ rescue StandardError => e
81
+ Legate.logger.error("Failed to clear all tokens: #{e.message}")
82
+ false
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'runner'
4
+ require_relative 'token_manager'
5
+ require_relative 'token_store'
6
+
7
+ module Legate
8
+ module Auth
9
+ # Extension for Legate::ToolContext that adds fiber-based authentication support
10
+ # This module is meant to be included in the Legate::ToolContext class to add
11
+ # authentication-related methods for tools.
12
+ module ToolContextExtension
13
+ # Get or create an authentication runner for this context
14
+ # @return [Legate::Auth::Runner] The authentication runner
15
+ def auth_runner
16
+ @auth_runner ||= begin
17
+ # Create the token store
18
+ token_store = get_token_store
19
+
20
+ # Create a token manager
21
+ token_manager = Legate::Auth::TokenManager.new(token_store)
22
+
23
+ # Create the runner
24
+ Legate::Auth::Runner.new(
25
+ session_service: session_service,
26
+ token_store: token_store,
27
+ token_manager: token_manager
28
+ )
29
+ end
30
+ end
31
+
32
+ # Get a token store for this context
33
+ # @return [Legate::Auth::TokenStore] The token store
34
+ def get_token_store
35
+ @token_store ||= if session_service.respond_to?(:scoped_state_container)
36
+ Legate::Auth::TokenStore.new(session_service)
37
+ elsif defined?(Legate::Auth) && Legate::Auth.respond_to?(:token_store)
38
+ Legate::Auth.token_store
39
+ else
40
+ Legate::Auth::TokenStore.new
41
+ end
42
+ end
43
+
44
+ # Run a block with authentication support
45
+ # @param handler [Proc, nil] Optional handler for authentication requests
46
+ # @yield The block to run
47
+ # @return [Object] The result of the block
48
+ def with_authentication(&block)
49
+ raise ArgumentError, 'Block is required' unless block_given?
50
+
51
+ # Get or create the authentication runner
52
+ runner = auth_runner
53
+
54
+ # Run the block with authentication support
55
+ runner.run(block, self) do |auth_request|
56
+ # Here we return nil to indicate that the auth request should be yielded
57
+ # to the tool's caller for handling. In a real implementation, this could
58
+ # handle authentication UI or delegate to another component.
59
+ nil
60
+ end
61
+ end
62
+
63
+ # Start an authentication session
64
+ # @param scheme [Legate::Auth::Scheme] The authentication scheme to use
65
+ # @param credential [Legate::Auth::Credential] The credential to use
66
+ # @param options [Hash] Additional options for the authentication session
67
+ # @return [Legate::Auth::ExchangedCredential] The authenticated credential
68
+ def auth_session(scheme, credential, **options)
69
+ # This method will be dynamically replaced by the auth_runner when
70
+ # running in a fiber context. This implementation is just for fallback
71
+ # when not running in a fiber.
72
+ raise NotImplementedError, 'Authentication session not available outside of with_authentication block'
73
+ end
74
+
75
+ # Handle an authentication response (for tools that handle responses)
76
+ # @param request_id [String] The request ID
77
+ # @param response [Hash] The response
78
+ # @return [Hash] The result of handling the response
79
+ def handle_auth_response(request_id, response)
80
+ runner = auth_runner
81
+ runner.handle_auth_response(request_id, response)
82
+ end
83
+
84
+ # Cancel an authentication flow (for tools that handle responses)
85
+ # @param request_id [String] The request ID
86
+ # @param reason [String, nil] Optional reason for cancellation
87
+ # @return [Boolean] True if the flow was successfully cancelled
88
+ def cancel_auth_flow(request_id, reason = nil)
89
+ runner = auth_runner
90
+ runner.cancel_auth_flow(request_id, reason)
91
+ end
92
+ end
93
+ end
94
+ end
95
+
96
+ # Extend the ToolContext class if it's already defined
97
+ Legate::ToolContext.include(Legate::Auth::ToolContextExtension) if defined?(Legate::ToolContext)