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,144 @@
1
+ / File: lib/legate/web/views/layout.slim
2
+ doctype html
3
+ html
4
+ head
5
+ title Legate
6
+ meta charset="utf-8"
7
+ meta name="viewport" content="width=device-width, initial-scale=1"
8
+ meta name="csrf-token" content=csrf_token
9
+ link rel="icon" type="image/png" sizes="32x32" href="/images/favicon-32.png"
10
+ link rel="icon" type="image/png" sizes="256x256" href="/images/favicon-256.png"
11
+ link rel="apple-touch-icon" href="/images/favicon-256.png"
12
+ link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css"
13
+ link rel="preconnect" href="https://fonts.googleapis.com"
14
+ link rel="preconnect" href="https://fonts.gstatic.com" crossorigin=""
15
+ link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@500;600;700&family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap"
16
+ link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" integrity="sha512-DTOQO9RWCH3ppGqcWaEA1BIZOC6xxalwEsw9c2QQeAIftl+Vegovlnee1c9QX4TctnWMn13TZye+giMm8e2LwA==" crossorigin="anonymous" referrerpolicy="no-referrer"
17
+ link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/codemirror.min.css"
18
+ link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/addon/lint/lint.css"
19
+ link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css" media="(prefers-color-scheme: light)" data-hljs-theme="light"
20
+ link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css" media="(prefers-color-scheme: dark)" data-hljs-theme="dark"
21
+ - css_v = (File.mtime(File.join(settings.public_folder, 'css', 'main.css')).to_i rescue Legate::VERSION)
22
+ link rel="stylesheet" href="/css/main.css?v=#{css_v}"
23
+
24
+ /! Theme initialization - runs early to prevent flash of wrong theme
25
+ script type="text/javascript"
26
+ | (function(){var t=localStorage.getItem('legate-theme');var s=window.matchMedia('(prefers-color-scheme:dark)').matches;document.documentElement.setAttribute('data-theme',t||(s?'dark':'light'));})();
27
+
28
+ script src="https://unpkg.com/htmx.org@1.9.10"
29
+ script src="https://unpkg.com/hyperscript.org@0.9.12"
30
+ script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/codemirror.min.js"
31
+ script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/mode/javascript/javascript.min.js"
32
+ script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/mode/markdown/markdown.min.js"
33
+ script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/addon/lint/lint.min.js"
34
+ script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/addon/lint/json-lint.min.js"
35
+ script src="https://unpkg.com/jsonlint@1.6.3/web/jsonlint.js"
36
+ script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"
37
+ script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/ruby.min.js"
38
+ script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/json.min.js"
39
+ script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/bash.min.js"
40
+ /! Add Mermaid.js library from CDN
41
+ script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"
42
+
43
+ /! Determine current section for nav highlighting
44
+ - current_path = request.path_info
45
+ - is_agents_section = current_path.start_with?('/agents')
46
+ - is_tools_section = current_path.start_with?('/tools')
47
+ - is_auth_section = current_path.start_with?('/auth')
48
+ - is_docs_section = current_path.start_with?('/docs')
49
+
50
+ body
51
+ /! Mobile top bar (touch only) — desktop uses the fixed sidebar
52
+ .mobile-topbar.is-hidden-desktop
53
+ a.brand-logo href="/"
54
+ img.brand-logo-img.brand-logo-light src="/images/legate-logo-light.png" alt="Legate"
55
+ img.brand-logo-img.brand-logo-dark src="/images/legate-logo-dark.png" alt="Legate"
56
+ button.sidebar-burger(aria-label="menu" _="on click toggle .is-active on #app-sidebar then toggle .is-active on #sidebar-backdrop")
57
+ span(aria-hidden="true")
58
+ span(aria-hidden="true")
59
+ span(aria-hidden="true")
60
+
61
+ aside#app-sidebar.sidebar
62
+ .sidebar-brand
63
+ a.brand-logo href="/"
64
+ img.brand-logo-img.brand-logo-light src="/images/legate-logo-light.png" alt="Legate"
65
+ img.brand-logo-img.brand-logo-dark src="/images/legate-logo-dark.png" alt="Legate"
66
+ p.sidebar-tagline Legion Commander
67
+ nav.sidebar-nav(role="navigation" aria-label="main navigation")
68
+ a.sidebar-link class=("is-active" if is_agents_section) href="/agents"
69
+ span.icon
70
+ i.fas.fa-robot
71
+ span Agents
72
+ a.sidebar-link class=("is-active" if is_tools_section) href="/tools"
73
+ span.icon
74
+ i.fas.fa-screwdriver-wrench
75
+ span Tools
76
+ a.sidebar-link class=("is-active" if is_auth_section) href="/auth"
77
+ span.icon
78
+ i.fas.fa-key
79
+ span Authentication
80
+ a.sidebar-link class=("is-active" if is_docs_section) href="/docs"
81
+ span.icon
82
+ i.fas.fa-book-open
83
+ span Documentation
84
+ .sidebar-footer
85
+ button.theme-toggle(onclick="toggleTheme()" aria-label="Toggle theme" title="Toggle dark/light mode")
86
+ i.fas.fa-moon
87
+ a.sidebar-ghlink href="https://github.com/tweibley/legate" target="_blank" title="GitHub"
88
+ span.icon
89
+ i.fab.fa-github
90
+ span GitHub
91
+
92
+ #sidebar-backdrop.sidebar-backdrop(_="on click remove .is-active from #app-sidebar then remove .is-active from me")
93
+
94
+ .app-content
95
+ main role="main"
96
+ section.section
97
+ .container
98
+ #toast-container style="position: fixed; top: 1em; right: 1em; z-index: 9999; width: 300px;"
99
+ #flash-messages
100
+ == yield
101
+
102
+ #health-check-modal.modal
103
+ .modal-background
104
+ .modal-content
105
+ .box.has-text-centered
106
+ span.icon.is-large.has-text-warning.mb-4
107
+ i.fas.fa-exclamation-triangle.fa-3x
108
+ p.is-size-5 Server Unavailable
109
+ p.has-text-grey Attempting to reconnect...
110
+ progress.progress.is-small.is-warning.mt-4 max="100"
111
+
112
+ /! --- MERMAID MODAL STRUCTURE ---
113
+ #mermaid-modal.modal
114
+ .modal-background.modal-close-button
115
+ .modal-card style="width: 80%; max-width: 1000px;"
116
+ header.modal-card-head
117
+ p.modal-card-title Agent Execution Flow
118
+ button.delete.modal-close-button aria-label="close"
119
+ section.modal-card-body
120
+ /! Content area for the Mermaid diagram
121
+ #mermaid-modal-content style="text-align: center; min-height: 200px;"
122
+ /! The pre.mermaid tag will be dynamically inserted here by JavaScript
123
+ footer.modal-card-foot
124
+ button.button.modal-close-button Close
125
+ /! --- END MERMAID MODAL ---
126
+
127
+ footer.footer.mt-6
128
+ .content.has-text-centered
129
+ p.is-size-7
130
+ span.icon.is-small style="color: var(--color-primary); vertical-align: middle;"
131
+ i.fas.fa-gem
132
+ strong Legate
133
+ span.has-text-grey-light · AI Agent Framework for Ruby
134
+ p.is-size-7.footer-links
135
+ a href="/docs" Documentation
136
+ span.has-text-grey-lighter |
137
+ a href="https://github.com/tweibley/legate" target="_blank"
138
+ span.icon.is-small
139
+ i.fab.fa-github
140
+ span GitHub
141
+
142
+ /! Framework JS: CSRF, toasts, editors, mermaid, htmx hooks, client helpers
143
+ - js_v = (File.mtime(File.join(settings.public_folder, 'js', 'legate.js')).to_i rescue Legate::VERSION)
144
+ script src="/js/legate.js?v=#{js_v}"
@@ -0,0 +1,87 @@
1
+ / File: lib/legate/web/views/tool_detail.slim
2
+ - if @tool
3
+
4
+ .content.mb-5
5
+ h1.title.is-2
6
+ = @tool[:name]
7
+ - if @tool_source
8
+ span.tag.is-info.is-light.ml-3 = @tool_source
9
+
10
+ - if @tool[:description]
11
+ p.subtitle
12
+ = @tool[:description]
13
+ - else
14
+ p.subtitle.has-text-grey
15
+ em No description provided.
16
+
17
+ .box.mb-5
18
+ h2.title.is-4 Parameters
19
+ p.subtitle.is-6 Input parameters this tool accepts.
20
+
21
+ - if @tool[:parameters].nil? || @tool[:parameters].empty?
22
+ .notification.is-light
23
+ | This tool doesn't require any parameters.
24
+ - else
25
+ .table-container
26
+ table.table.is-striped.is-hoverable.is-fullwidth
27
+ thead
28
+ tr
29
+ th Name
30
+ th Type
31
+ th Required
32
+ th Description
33
+ tbody
34
+ - @tool[:parameters].each do |param|
35
+ tr
36
+ td style="vertical-align: top;"
37
+ code = param[:name]
38
+ td style="vertical-align: top;"
39
+ span.tag.is-family-code = param[:type]
40
+ td style="vertical-align: top;"
41
+ - if param[:required]
42
+ span.tag.is-danger.is-light Required
43
+ - else
44
+ span.tag.is-light Optional
45
+ td = param[:description]
46
+
47
+ .box.mb-5
48
+ h2.title.is-4 Tool Execution Example
49
+ p.subtitle.is-6 Example of how to call this tool.
50
+
51
+ .columns
52
+ .column
53
+ h3.title.is-5 Input
54
+ pre.has-background-dark.has-text-light.p-3.is-family-code style="border-radius: 6px;"
55
+ | {
56
+ | "tool": "#{@tool[:name]}",
57
+ - if @tool[:parameters] && !@tool[:parameters].empty?
58
+ | "parameters": {
59
+ - @tool[:parameters].each_with_index do |param, index|
60
+ | "#{param[:name]}": #{param[:type] == 'string' ? '"example-value"' : param[:type] == 'number' ? '42' : param[:type] == 'boolean' ? 'true' : '{}'}#{index < @tool[:parameters].length - 1 ? ',' : ''}
61
+ | }
62
+ - else
63
+ | "parameters": {}
64
+ | }
65
+
66
+ - if @tool[:outputSchema]
67
+ .column
68
+ h3.title.is-5 Output Schema
69
+ pre.has-background-dark.has-text-light.p-3.is-family-code style="border-radius: 6px;"
70
+ = JSON.pretty_generate(@tool[:outputSchema])
71
+
72
+ .buttons.mt-5
73
+ - if @tool[:source] == :native
74
+ a.button.is-link(href="/tools/#{@tool[:name]}/download" download="#{@tool[:name]}.rb")
75
+ span.icon
76
+ i.fas.fa-file-code
77
+ span Download Ruby
78
+ a.button.is-light href="/tools"
79
+ span.icon
80
+ i.fas.fa-arrow-left
81
+ span Back to Tools List
82
+
83
+ - else
84
+ .notification.is-danger
85
+ | Tool not found. It may have been removed or renamed.
86
+ br
87
+ a href="/tools" Return to tool list
@@ -0,0 +1,50 @@
1
+ / File: lib/legate/web/views/tools.slim
2
+ .content.mb-5
3
+ h1.title.is-2 Tools
4
+ p.subtitle Browse available tools that can be used by agents.
5
+
6
+ / AI Generator button - prominent placement
7
+ .buttons.mb-4
8
+ button.button.is-medium.ai-generate-btn(onclick="openToolGeneratorModal()")
9
+ span.icon
10
+ i.fas.fa-wand-magic-sparkles
11
+ span
12
+ strong Build with AI Builder
13
+
14
+ .box
15
+ h2.title.is-4 Available Native Tools
16
+ p.subtitle.is-6 Shows tools built directly into this Legate application.
17
+
18
+ - if @native_tools.nil? || @native_tools.empty?
19
+ .notification.is-warning No native tools found.
20
+ - else
21
+ .table-container
22
+ table.table.is-striped.is-hoverable.is-fullwidth
23
+ thead
24
+ tr
25
+ th Name
26
+ th Description
27
+ th Parameters
28
+ tbody
29
+ - @native_tools.each do |tool|
30
+ tr
31
+ td style="vertical-align: top;"
32
+ a href="/tools/#{tool[:name]}"
33
+ strong = tool[:name]
34
+ td style="vertical-align: top;"
35
+ = tool[:description] || content_tag(:em, "[no description provided]")
36
+ td
37
+ - if tool[:parameters].empty?
38
+ em None
39
+ - else
40
+ dl.parameters-list
41
+ - tool[:parameters].each do |param|
42
+ dt
43
+ code = param[:name]
44
+ span.tag.is-light.is-family-code.ml-1 = param[:type]
45
+ - if param[:required]
46
+ span.tag.is-danger.is-light.is-small.ml-1 Required
47
+ dd = param[:description]
48
+
49
+ / Include the AI Tool Generator Modal
50
+ == slim :_tool_generator_modal, layout: false
@@ -0,0 +1,367 @@
1
+ # File: lib/legate/web/webhook_listener.rb
2
+ # frozen_string_literal: true
3
+
4
+ require 'sinatra/base'
5
+ require 'sinatra/json'
6
+ require 'sinatra/custom_logger' # For `helpers Sinatra::CustomLogger` (no longer autoloaded in sinatra-contrib 4)
7
+ require 'json'
8
+ require 'concurrent'
9
+ require 'securerandom'
10
+ require 'legate' # To access Legate.config, Legate.logger
11
+ require 'legate/errors' # For Legate::WebhookConfigurationError
12
+ require_relative '../global_definition_registry'
13
+ require 'mustermann' # For path pattern matching
14
+
15
+ module Legate
16
+ module Web
17
+ # Minimal Rack application (using Sinatra) to listen for incoming webhooks.
18
+ # Intended to be mounted within the main Legate::Web::App or run standalone.
19
+ class WebhookListener < Sinatra::Base
20
+ helpers Sinatra::CustomLogger # Use Legate.logger
21
+
22
+ MAX_REQUEST_BODY_SIZE = 10 * 1024 * 1024 # 10 MB
23
+
24
+ configure do
25
+ set :logger, Legate.logger
26
+ # Webhooks arrive from external services with arbitrary Host headers, so
27
+ # permit all hosts (Sinatra 4 / rack-protection 4 enable Host
28
+ # authorization by default, which would otherwise 403 them).
29
+ set :host_authorization, { permitted_hosts: [] }
30
+ # Disable Sinatra's built-in error handling to provide custom responses
31
+ set :show_exceptions, false
32
+ set :raise_errors, false # Let our error handler catch them
33
+ # Prevent Sinatra from starting its own server if run directly (we mount it)
34
+ set :server, :noop
35
+ end
36
+
37
+ # --- Instance Initialization ---
38
+ def initialize(app = nil)
39
+ super(app) # Call Sinatra::Base initializer
40
+ setup_static_routes! # Setup routes on instance creation
41
+ end
42
+ # ---------------------------
43
+
44
+ # --- Middleware/Hooks ---
45
+
46
+ before do
47
+ env['webhook.request_id'] = SecureRandom.uuid
48
+
49
+ # Reject oversized request bodies to prevent memory exhaustion
50
+ content_length = request.content_length.to_i
51
+ halt 413, json({ status: :error, error_message: 'Request body too large' }) if content_length > MAX_REQUEST_BODY_SIZE
52
+
53
+ # Ensure request body is parsed as JSON if content type indicates it
54
+ if request.content_type&.match?(%r{application/json}i)
55
+ request.body.rewind
56
+ begin
57
+ raw_body = request.body.read(MAX_REQUEST_BODY_SIZE + 1)
58
+ halt 413, json({ status: :error, error_message: 'Request body too large' }) if raw_body && raw_body.bytesize > MAX_REQUEST_BODY_SIZE
59
+ # Store parsed body in rack env for handlers to access
60
+ env['rack.input.json'] = JSON.parse(raw_body || '')
61
+ rescue JSON::ParserError => e
62
+ request_id = env['webhook.request_id']
63
+ logger.warn("WebhookListener [#{request_id}]: Invalid JSON received: #{e.message}")
64
+ halt 400, json({ status: :error, error_message: 'Invalid JSON format', request_id: request_id })
65
+ ensure
66
+ request.body.rewind # Ensure body is readable again for potential validation
67
+ end
68
+ end
69
+ end
70
+
71
+ # --- Error Handling ---
72
+
73
+ error Legate::WebhookConfigurationError do
74
+ e = env['sinatra.error']
75
+ request_id = env['webhook.request_id']
76
+ logger.warn("Webhook Configuration Error [#{request_id}]: #{e.message}")
77
+ content_type :json
78
+ status 400
79
+ json({ status: :error, error_message: "Configuration Error: #{e.message}", request_id: request_id })
80
+ end
81
+
82
+ error 400..599 do # Catch common client/server errors
83
+ content_type :json
84
+ # Use response body if already set (e.g., by halt), otherwise generic message
85
+ response_body = response.body.first
86
+ if response_body && !response_body.empty?
87
+ response_body
88
+ else
89
+ status_message = Rack::Utils::HTTP_STATUS_CODES[response.status] || 'Error'
90
+ json({ status: :error, error_message: "Webhook Error: #{status_message} (Status #{response.status})" })
91
+ end
92
+ end
93
+
94
+ error StandardError do
95
+ e = env['sinatra.error']
96
+ request_id = env['webhook.request_id']
97
+ logger.error("WebhookListener Internal Error [#{request_id}]: #{e.class} - #{e.message}")
98
+ logger.error(e.backtrace.join("\n"))
99
+ content_type :json
100
+ status 500
101
+ json({ status: :error, error_message: 'Internal Server Error', request_id: request_id })
102
+ end
103
+
104
+ # --- Routing ---
105
+
106
+ # Dynamic route handler using pattern matching
107
+ post '*' do
108
+ webhook_config = Legate.config.webhooks
109
+ agent_name_sym = nil
110
+
111
+ # Match request path against configured pattern
112
+ configured_pattern = webhook_config.dynamic_agent_route_pattern
113
+ pattern = Mustermann.new(configured_pattern, type: :sinatra)
114
+ match_params = pattern.params(request.path_info)
115
+
116
+ # Only proceed if the pattern matches
117
+ unless match_params
118
+ return pass # Didn't match dynamic pattern, try other routes (static, not_found)
119
+ end
120
+
121
+ # Pattern matched. Now check if handler enabled.
122
+ unless webhook_config.enable_dynamic_agent_handler
123
+ logger.warn('Webhook dynamic route matched, but handler is disabled.')
124
+ halt 403, json({ status: :error, error_message: 'Dynamic agent webhooks are disabled.' }) # Explicit 403
125
+ end
126
+
127
+ # Handler enabled and pattern matched. Extract agent name.
128
+ agent_name_param = match_params['agent_name']
129
+ if agent_name_param
130
+ # Validate against registered definitions before converting to Symbol
131
+ # to prevent Symbol table exhaustion from arbitrary URL paths
132
+ registered = Legate::GlobalDefinitionRegistry.all
133
+ unless registered.keys.map(&:to_s).include?(agent_name_param.to_s)
134
+ logger.warn("WebhookListener: Unknown agent '#{agent_name_param}' in webhook URL")
135
+ halt 404, json({ status: :error, error_message: 'Agent not found' })
136
+ end
137
+ agent_name_sym = agent_name_param.to_sym
138
+ else
139
+ logger.error("Webhook dynamic route matched, but required 'agent_name' parameter missing in pattern or path.")
140
+ # Consider this a server config error if name is expected but missing
141
+ halt 500, json({ status: :error, error_message: 'Internal Server Error: Route configuration issue.' })
142
+ end
143
+
144
+ # --- Handler Logic (Agent name confirmed) ---
145
+ # Read raw body *first* for validation purposes
146
+ request.body.rewind
147
+ raw_request_body = request.body.read
148
+ request.body.rewind # Rewind again for potential JSON parsing or handler use
149
+
150
+ parsed_json_body = env['rack.input.json'] # Parsed by 'before' hook if Content-Type was JSON
151
+
152
+ logger.info("WebhookListener: Processing dynamic agent trigger for: #{agent_name_sym}")
153
+
154
+ # Load agent definition from GlobalDefinitionRegistry
155
+ in_memory_definition = Legate::GlobalDefinitionRegistry.find(agent_name_sym)
156
+
157
+ unless in_memory_definition
158
+ logger.error("WebhookListener: Definition for :#{agent_name_sym} not found in GlobalDefinitionRegistry.")
159
+ halt 500, json({ status: :error, error_message: 'Internal Server Error: Agent definition not loaded.' })
160
+ end
161
+
162
+ # Check webhook_enabled on the definition object
163
+ unless in_memory_definition.webhook_enabled
164
+ logger.warn("Agent '#{agent_name_sym}' is not enabled for webhooks (webhook_enabled=false).")
165
+ halt 404, json({ status: :error, error_message: 'Webhook endpoint not found for this agent.' })
166
+ end
167
+
168
+ # Perform Validation
169
+ validator_config = in_memory_definition.webhook_validator || webhook_config.global_validator
170
+ # Use agent secret, falling back to global secret if available
171
+ secret = in_memory_definition.webhook_secret || webhook_config.global_secret
172
+ if validator_config
173
+ validator_proc = validator_config.is_a?(Proc) ? validator_config : webhook_config.find_validator(validator_config)
174
+
175
+ if validator_proc.nil?
176
+ logger.error("Webhook validation failed for '#{agent_name_sym}': Validator '#{validator_config}' not found.")
177
+ halt 500, json({ status: :error, error_message: 'Internal Server Error: Validator configuration issue.' })
178
+ end
179
+
180
+ begin
181
+ is_valid = validator_proc.call(request, secret)
182
+ unless is_valid
183
+ logger.warn("Webhook validation failed for agent '#{agent_name_sym}'.")
184
+ halt 401,
185
+ json({ status: :error, error_message: 'Unauthorized: Invalid request signature or credentials.' })
186
+ end
187
+ logger.debug("Webhook validation successful for agent '#{agent_name_sym}'.")
188
+ rescue StandardError => e
189
+ logger.error("Error during webhook validation for '#{agent_name_sym}': #{e.message}")
190
+ halt 500, json({ status: :error, error_message: 'Internal Server Error during validation.' })
191
+ end
192
+ else
193
+ logger.debug("No validator configured for agent '#{agent_name_sym}', skipping validation.")
194
+ end
195
+
196
+ # 5. Perform Transformation (Required if webhook_enabled is true)
197
+ # --- USE IN-MEMORY DEFINITION FOR PROC --- #
198
+ transformer = in_memory_definition.webhook_transformer
199
+ unless transformer.is_a?(Proc)
200
+ logger.error("Webhook configuration error for '#{agent_name_sym}': Missing webhook_transformer Proc in in-memory definition.")
201
+ halt 500,
202
+ json({ status: :error,
203
+ error_message: 'Internal Server Error: Agent webhook configuration incomplete (transformer).' })
204
+ end
205
+
206
+ begin
207
+ # Pass the parsed JSON body if available, otherwise the raw body string
208
+ payload_for_transform = parsed_json_body || raw_request_body
209
+ transformed_user_input = transformer.call(payload_for_transform)
210
+ logger.debug("Webhook payload transformed successfully for agent '#{agent_name_sym}'.")
211
+ rescue Legate::WebhookConfigurationError => e
212
+ # Re-raise specific config errors to be caught by dedicated handler
213
+ raise e
214
+ rescue StandardError => e
215
+ logger.error("Error during webhook transformation for '#{agent_name_sym}': #{e.class} - #{e.message}")
216
+ halt 500, json({ status: :error, error_message: 'Internal Server Error during payload transformation.' })
217
+ end
218
+
219
+ # 6. Extract Session ID (Required if webhook_enabled is true)
220
+ # --- USE IN-MEMORY DEFINITION FOR PROC --- #
221
+ extractor = in_memory_definition.webhook_session_extractor
222
+ unless extractor.is_a?(Proc)
223
+ logger.error("Webhook configuration error for '#{agent_name_sym}': Missing webhook_session_extractor Proc in in-memory definition.")
224
+ halt 500,
225
+ json({ status: :error,
226
+ error_message: 'Internal Server Error: Agent webhook configuration incomplete (session extractor).' })
227
+ end
228
+
229
+ begin
230
+ # Pass the parsed JSON body if available, otherwise the raw body string
231
+ payload_for_extract = parsed_json_body || raw_request_body
232
+ session_id = extractor.call(payload_for_extract)
233
+ unless session_id.is_a?(String) && !session_id.strip.empty?
234
+ raise Legate::WebhookConfigurationError,
235
+ 'Session extractor must return a non-empty String session ID.'
236
+ end
237
+
238
+ logger.debug("Webhook session ID extracted successfully for agent '#{agent_name_sym}': #{session_id}")
239
+ rescue Legate::WebhookConfigurationError => e
240
+ # Re-raise specific config errors to be caught by dedicated handler
241
+ raise e
242
+ rescue StandardError => e
243
+ logger.error("Error during webhook session extraction for '#{agent_name_sym}': #{e.class} - #{e.message}")
244
+ halt 500, json({ status: :error, error_message: 'Internal Server Error during session ID extraction.' })
245
+ end
246
+
247
+ # 7. Spawn threaded task
248
+ begin
249
+ task_id = SecureRandom.uuid
250
+ session_service = Legate.config.session_service
251
+
252
+ # Ensure session exists in the shared service
253
+ existing = session_service.get_session(session_id: session_id)
254
+ unless existing
255
+ session_service.create_session(
256
+ app_name: agent_name_sym.to_s,
257
+ user_id: 'webhook',
258
+ session_id: session_id
259
+ )
260
+ end
261
+
262
+ Concurrent::Promises.future do
263
+ definition = Legate::GlobalDefinitionRegistry.find(agent_name_sym)
264
+ agent = Legate::Agent.new(definition: definition, session_service: session_service)
265
+ agent.start
266
+ agent.run_task(session_id: session_id, user_input: transformed_user_input, session_service: session_service)
267
+ rescue StandardError => e
268
+ Legate.logger.error("Webhook agent task failed for '#{agent_name_sym}': #{e.class} - #{e.message}")
269
+ Legate.logger.error(e.backtrace&.first(5)&.join("\n"))
270
+ end
271
+
272
+ logger.info("Webhook task spawned for agent '#{agent_name_sym}'. Session: #{session_id}, Task ID: #{task_id}")
273
+ rescue StandardError => e
274
+ logger.error("Unexpected error spawning webhook task for '#{agent_name_sym}': #{e.class} - #{e.message}")
275
+ halt 500, json({ status: :error, error_message: 'Internal Server Error during task spawning.' })
276
+ end
277
+
278
+ # 8. Return 202 Accepted
279
+ content_type :json
280
+ status 202
281
+ json({ status: :accepted, message: "Request for agent '#{agent_name_sym}' accepted and queued.",
282
+ task_id: task_id })
283
+ end
284
+
285
+ # Catch-all for undefined routes within the listener's base path
286
+ not_found do
287
+ content_type :json
288
+ # Only set body if not already set by a specific halt
289
+ if response.body.empty?
290
+ json({ status: :error,
291
+ error_message: "Webhook route not found: #{request.request_method} #{request.path_info}" })
292
+ end
293
+ status 404 # Ensure status is 404
294
+ end
295
+
296
+ private
297
+
298
+ # --- Instance method to set up static routes ---
299
+ def setup_static_routes!
300
+ webhook_config = Legate.config.webhooks
301
+ logger = Legate.logger # Use instance logger helper
302
+
303
+ webhook_config.static_routes.each do |method_path, route_config|
304
+ method, path = method_path.split(' ', 2)
305
+ http_method = method.downcase.to_sym
306
+
307
+ unless %i[get post put patch delete head options].include?(http_method)
308
+ logger.error("WebhookListener: Invalid HTTP method '#{method}' specified for static route '#{path}'. Skipping.")
309
+ next
310
+ end
311
+ unless route_config.handler.is_a?(Proc)
312
+ logger.error("WebhookListener: Invalid handler (not a Proc) for static route '#{method_path}'. Skipping.")
313
+ next
314
+ end
315
+
316
+ logger.debug("WebhookListener: Defining static route: #{http_method.upcase} #{path}")
317
+
318
+ # Use Sinatra's instance-level routing DSL (get, post, etc.)
319
+ self.class.send(http_method, path) do |*route_params|
320
+ # Re-fetch config inside route block in case it changed?
321
+ # Or rely on config captured during initialization?
322
+ # Let's assume config is stable after init for simplicity.
323
+
324
+ # --- Validation Logic ---
325
+ current_validator_config = route_config.validator # Use captured route_config
326
+ current_secret = route_config.secret
327
+ if current_validator_config
328
+ current_validator_proc = current_validator_config.is_a?(Proc) ? current_validator_config : webhook_config.find_validator(current_validator_config)
329
+ if current_validator_proc.nil?
330
+ logger.error("Static Route Validation Error [#{method_path}]: Validator '#{current_validator_config}' not found.")
331
+ halt 500,
332
+ json({ status: :error,
333
+ error_message: 'Internal Server Error: Static route validator configuration issue.' })
334
+ end
335
+ begin
336
+ is_valid = current_validator_proc.call(request, current_secret)
337
+ unless is_valid
338
+ logger.warn("Static Route Validation Failed [#{method_path}]")
339
+ halt 401,
340
+ json({ status: :error,
341
+ error_message: 'Unauthorized: Invalid request signature or credentials.' })
342
+ end
343
+ logger.debug("Static Route Validation OK [#{method_path}]")
344
+ rescue StandardError => e
345
+ logger.error("Error during static route validation [#{method_path}]: #{e.message}")
346
+ halt 500,
347
+ json({ status: :error, error_message: 'Internal Server Error during static route validation.' })
348
+ end
349
+ end
350
+ # --- End Validation Logic ---
351
+
352
+ # Execute the handler proc
353
+ begin
354
+ # Pass route params along with request to handler? Handler signature is just `call(request)` for now.
355
+ route_config.handler.call(request)
356
+ rescue StandardError => e
357
+ logger.error("Error executing static route handler [#{method_path}]: #{e.class} - #{e.message}")
358
+ logger.error(e.backtrace.join("\n"))
359
+ halt 500, json({ status: :error, error_message: 'Internal Server Error in static route handler.' })
360
+ end
361
+ end
362
+ end
363
+ end
364
+ # --- End instance method ---
365
+ end
366
+ end
367
+ end
data/lib/legate/web.rb ADDED
@@ -0,0 +1,9 @@
1
+ # File: lib/legate/web.rb
2
+ # frozen_string_literal: true
3
+
4
+ # Opt-in entry point for the Legate web UI. Require this (not 'legate' alone)
5
+ # when you want the Sinatra app and webhook listener. Keeps the web stack
6
+ # (Sinatra, Puma, Slim, sass-embedded) out of the core library load path.
7
+ require_relative '../legate' unless defined?(Legate::Agent)
8
+ require_relative 'web/app'
9
+ require_relative 'web/webhook_listener'