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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +345 -0
- data/bin/legate +13 -0
- data/examples/00_quickstart.rb +51 -0
- data/examples/01_simple_agent.rb +105 -0
- data/examples/02_multi_tool_agent.rb +140 -0
- data/examples/03_custom_tool.rb +93 -0
- data/examples/04_agent_instructions.rb +84 -0
- data/examples/05_state_and_sessions.rb +91 -0
- data/examples/06_callbacks.rb +186 -0
- data/examples/07_async_jobs.rb +112 -0
- data/examples/08_loop_agent.rb +197 -0
- data/examples/09_sequential_workflow.rb +40 -0
- data/examples/10_parallel_workflow.rb +34 -0
- data/examples/11_agent_delegation.rb +24 -0
- data/examples/12_http_client_tool.rb +156 -0
- data/examples/13_authentication.rb +220 -0
- data/examples/14_mcp_client.rb +154 -0
- data/examples/15_mcp_server.rb +79 -0
- data/examples/16_webhooks.rb +91 -0
- data/examples/README_sequential_agents.md +164 -0
- data/examples/advanced/auth/cookie_auth_tool.rb +146 -0
- data/examples/advanced/auth/custom_auth_flows_example.rb +626 -0
- data/examples/advanced/auth/excon_middleware.rb +317 -0
- data/examples/advanced/auth/excon_middleware_auth.rb +399 -0
- data/examples/advanced/auth/fiber_auth_example.rb +281 -0
- data/examples/advanced/auth/fiber_oidc_example.rb +403 -0
- data/examples/advanced/auth/httpbin_bearer_tool.rb +159 -0
- data/examples/advanced/auth/oauth2_auth.rb +419 -0
- data/examples/advanced/auth/oidc_auth.rb +514 -0
- data/examples/advanced/auth/openweather_api.rb +251 -0
- data/examples/advanced/auth/openweather_tool.rb +153 -0
- data/examples/advanced/auth/query_param_middleware_test.rb +138 -0
- data/examples/advanced/auth/service_account.rb +135 -0
- data/examples/advanced/auth/test_with_httpbin.rb +202 -0
- data/examples/advanced/auth/token_lifecycle_example.rb +428 -0
- data/examples/advanced/callback_monitoring.rb +679 -0
- data/examples/advanced/mas/fixed_delegation_example.rb +191 -0
- data/examples/advanced/mas/loop_workflow.rb +28 -0
- data/examples/advanced/mas/mock_planner.rb +77 -0
- data/examples/advanced/mas/proper_delegation_example.rb +276 -0
- data/examples/advanced/mcp/legate_mcp_server_resource_example.rb +182 -0
- data/examples/advanced/mcp/mcp_resource_server_example.rb +309 -0
- data/examples/advanced/mcp/mcp_server_async.rb +76 -0
- data/examples/advanced/mcp/mcp_server_async_tools.rb +122 -0
- data/examples/advanced/mcp/mcp_server_legate_agent.rb +95 -0
- data/examples/advanced/mcp/mcp_server_rack.rb +89 -0
- data/examples/advanced/random_calculator.rb +104 -0
- data/examples/advanced/sleep_agent.rb +153 -0
- data/examples/advanced/webhooks/webhook_e2e_runner.rb +110 -0
- data/examples/advanced/webhooks/webhook_receiver_agent.rb +58 -0
- data/examples/advanced/workflows/task_refinement_loop_agent.rb +278 -0
- data/examples/advanced/workflows/travel_planner_auto_sequential.rb +444 -0
- data/examples/advanced/workflows/travel_planner_parallel.rb +656 -0
- data/examples/advanced/workflows/travel_planner_sequential.rb +512 -0
- data/examples/tools/oauth2_example.rb +136 -0
- data/examples/tools/sleepy_tool.rb +42 -0
- data/lib/legate/activity_log.rb +71 -0
- data/lib/legate/agent.rb +959 -0
- data/lib/legate/agent_code_generator.rb +185 -0
- data/lib/legate/agent_definition.rb +812 -0
- data/lib/legate/agentic/decision.rb +49 -0
- data/lib/legate/agentic/loop.rb +134 -0
- data/lib/legate/agentic.rb +5 -0
- data/lib/legate/agents/loop_agent.rb +248 -0
- data/lib/legate/agents/parallel_agent.rb +163 -0
- data/lib/legate/agents/sequential_agent.rb +190 -0
- data/lib/legate/agents.rb +14 -0
- data/lib/legate/auth/config.rb +148 -0
- data/lib/legate/auth/coordinator.rb +218 -0
- data/lib/legate/auth/coordinators/oauth2_coordinator.rb +99 -0
- data/lib/legate/auth/coordinators/oidc_coordinator.rb +68 -0
- data/lib/legate/auth/coordinators/service_account_coordinator.rb +122 -0
- data/lib/legate/auth/credential.rb +157 -0
- data/lib/legate/auth/encryption.rb +108 -0
- data/lib/legate/auth/error.rb +94 -0
- data/lib/legate/auth/exchanged_credential.rb +180 -0
- data/lib/legate/auth/excon_middleware.rb +285 -0
- data/lib/legate/auth/http_client_utils.rb +364 -0
- data/lib/legate/auth/manager.rb +531 -0
- data/lib/legate/auth/manager_store.rb +394 -0
- data/lib/legate/auth/middleware_factory.rb +290 -0
- data/lib/legate/auth/runner.rb +279 -0
- data/lib/legate/auth/scheme.rb +125 -0
- data/lib/legate/auth/schemes/api_key.rb +212 -0
- data/lib/legate/auth/schemes/google_service_account.rb +108 -0
- data/lib/legate/auth/schemes/http_bearer.rb +98 -0
- data/lib/legate/auth/schemes/oauth2.rb +396 -0
- data/lib/legate/auth/schemes/openid_connect.rb +346 -0
- data/lib/legate/auth/schemes/service_account.rb +388 -0
- data/lib/legate/auth/schemes.rb +40 -0
- data/lib/legate/auth/token_manager.rb +362 -0
- data/lib/legate/auth/token_store.rb +86 -0
- data/lib/legate/auth/tool_context_extension.rb +97 -0
- data/lib/legate/auth/tool_integration.rb +188 -0
- data/lib/legate/auth/url_guard.rb +81 -0
- data/lib/legate/auth.rb +453 -0
- data/lib/legate/callbacks/callback_context.rb +71 -0
- data/lib/legate/cli/agent_commands.rb +950 -0
- data/lib/legate/cli/auth_commands.rb +520 -0
- data/lib/legate/cli/base_command.rb +24 -0
- data/lib/legate/cli/deployment_commands.rb +934 -0
- data/lib/legate/cli/output_helper.rb +108 -0
- data/lib/legate/cli/session_commands.rb +138 -0
- data/lib/legate/cli/skaffold_commands.rb +223 -0
- data/lib/legate/cli/tool_commands.rb +261 -0
- data/lib/legate/cli/web_commands.rb +182 -0
- data/lib/legate/cli.rb +40 -0
- data/lib/legate/configuration/webhooks.rb +113 -0
- data/lib/legate/configuration.rb +39 -0
- data/lib/legate/definition_store.rb +23 -0
- data/lib/legate/errors.rb +118 -0
- data/lib/legate/event.rb +161 -0
- data/lib/legate/gemini_ai_beta_patch.rb +39 -0
- data/lib/legate/generators/agent_generator.rb +412 -0
- data/lib/legate/generators/code_validator.rb +48 -0
- data/lib/legate/generators/legate/install_generator.rb +35 -0
- data/lib/legate/generators/legate/templates/create_legate_tables.rb.tt +36 -0
- data/lib/legate/generators/legate/templates/initializer.rb +18 -0
- data/lib/legate/generators/runtime_tool_loader.rb +76 -0
- data/lib/legate/generators/tool_generator.rb +408 -0
- data/lib/legate/generators.rb +11 -0
- data/lib/legate/global_definition_registry.rb +506 -0
- data/lib/legate/global_tool_manager.rb +135 -0
- data/lib/legate/llm/adapter.rb +69 -0
- data/lib/legate/llm/gemini.rb +172 -0
- data/lib/legate/llm/ollama.rb +80 -0
- data/lib/legate/llm.rb +34 -0
- data/lib/legate/mcp/client.rb +320 -0
- data/lib/legate/mcp/connection/sse.rb +292 -0
- data/lib/legate/mcp/connection/stdio.rb +273 -0
- data/lib/legate/mcp/connection_manager.rb +103 -0
- data/lib/legate/mcp/server/legate_agent_adapter.rb +170 -0
- data/lib/legate/mcp/server/legate_direct_agent_adapter.rb +140 -0
- data/lib/legate/mcp/server/legate_tool_adapter.rb +119 -0
- data/lib/legate/mcp/tool_wrapper.rb +138 -0
- data/lib/legate/mcp/util/schema_converter.rb +134 -0
- data/lib/legate/mcp.rb +23 -0
- data/lib/legate/plan_executor.rb +375 -0
- data/lib/legate/planner.rb +839 -0
- data/lib/legate/rails/railtie.rb +43 -0
- data/lib/legate/rails.rb +9 -0
- data/lib/legate/redaction.rb +32 -0
- data/lib/legate/session.rb +299 -0
- data/lib/legate/session_service/active_record.rb +300 -0
- data/lib/legate/session_service/base.rb +68 -0
- data/lib/legate/session_service/event_broadcast.rb +74 -0
- data/lib/legate/session_service/in_memory.rb +188 -0
- data/lib/legate/tool/metadata_dsl.rb +122 -0
- data/lib/legate/tool.rb +276 -0
- data/lib/legate/tool_code_generator.rb +103 -0
- data/lib/legate/tool_context.rb +350 -0
- data/lib/legate/tool_loader.rb +39 -0
- data/lib/legate/tool_registry.rb +73 -0
- data/lib/legate/tool_result.rb +61 -0
- data/lib/legate/tools/agent_tool.rb +187 -0
- data/lib/legate/tools/base/http_client.rb +319 -0
- data/lib/legate/tools/base/safe_url.rb +56 -0
- data/lib/legate/tools/base_async_job_tool.rb +91 -0
- data/lib/legate/tools/calculator.rb +89 -0
- data/lib/legate/tools/cat_facts.rb +81 -0
- data/lib/legate/tools/check_job_status_tool.rb +48 -0
- data/lib/legate/tools/current_time_tool.rb +64 -0
- data/lib/legate/tools/echo.rb +43 -0
- data/lib/legate/tools/http_request_tool.rb +105 -0
- data/lib/legate/tools/random_number_tool.rb +64 -0
- data/lib/legate/tools/read_webpage_tool.rb +92 -0
- data/lib/legate/tools/sleepy_tool.rb +74 -0
- data/lib/legate/tools/webhook_tool.rb +146 -0
- data/lib/legate/version.rb +5 -0
- data/lib/legate/web/app.rb +984 -0
- data/lib/legate/web/public/css/main.css +4980 -0
- data/lib/legate/web/public/images/favicon-256.png +0 -0
- data/lib/legate/web/public/images/favicon-32.png +0 -0
- data/lib/legate/web/public/images/legate-logo-dark.png +0 -0
- data/lib/legate/web/public/images/legate-logo-light.png +0 -0
- data/lib/legate/web/public/js/legate.js +616 -0
- data/lib/legate/web/public/styles/main.scss +4402 -0
- data/lib/legate/web/routes/agent_authentication_routes.rb +530 -0
- data/lib/legate/web/routes/agent_definition_routes.rb +803 -0
- data/lib/legate/web/routes/agent_generator_routes.rb +80 -0
- data/lib/legate/web/routes/agent_interaction_routes.rb +734 -0
- data/lib/legate/web/routes/agent_runtime_routes.rb +323 -0
- data/lib/legate/web/routes/api_routes.rb +56 -0
- data/lib/legate/web/routes/authentication_routes.rb +1541 -0
- data/lib/legate/web/routes/core_routes.rb +111 -0
- data/lib/legate/web/routes/documentation_routes.rb +220 -0
- data/lib/legate/web/routes/tool_generator_routes.rb +81 -0
- data/lib/legate/web/routes/tools_ui_routes.rb +207 -0
- data/lib/legate/web/sass_compiler.rb +73 -0
- data/lib/legate/web/views/_active_session_info.slim +25 -0
- data/lib/legate/web/views/_activity_list.slim +55 -0
- data/lib/legate/web/views/_agent_card.slim +56 -0
- data/lib/legate/web/views/_agent_generator_modal.slim +382 -0
- data/lib/legate/web/views/_agent_status_controls.slim +71 -0
- data/lib/legate/web/views/_agent_tool_table.slim +74 -0
- data/lib/legate/web/views/_chat_message.slim +95 -0
- data/lib/legate/web/views/_display_agent_configuration.slim +26 -0
- data/lib/legate/web/views/_display_agent_description.slim +11 -0
- data/lib/legate/web/views/_display_agent_fallback.slim +15 -0
- data/lib/legate/web/views/_display_agent_hierarchy.slim +93 -0
- data/lib/legate/web/views/_display_agent_instruction.slim +17 -0
- data/lib/legate/web/views/_display_agent_mcp.slim +13 -0
- data/lib/legate/web/views/_display_agent_model.slim +17 -0
- data/lib/legate/web/views/_display_agent_name.slim +42 -0
- data/lib/legate/web/views/_display_agent_output_key.slim +26 -0
- data/lib/legate/web/views/_display_agent_type.slim +65 -0
- data/lib/legate/web/views/_edit_agent_configuration.slim +74 -0
- data/lib/legate/web/views/_edit_agent_description.slim +16 -0
- data/lib/legate/web/views/_edit_agent_fallback.slim +25 -0
- data/lib/legate/web/views/_edit_agent_hierarchy.slim +98 -0
- data/lib/legate/web/views/_edit_agent_instruction.slim +49 -0
- data/lib/legate/web/views/_edit_agent_mcp.slim +33 -0
- data/lib/legate/web/views/_edit_agent_model.slim +23 -0
- data/lib/legate/web/views/_edit_agent_output_key.slim +36 -0
- data/lib/legate/web/views/_edit_agent_tools.slim +40 -0
- data/lib/legate/web/views/_edit_agent_type.slim +67 -0
- data/lib/legate/web/views/_session_error.slim +4 -0
- data/lib/legate/web/views/_skeleton.slim +69 -0
- data/lib/legate/web/views/_tool_card.slim +9 -0
- data/lib/legate/web/views/_tool_generator_modal.slim +311 -0
- data/lib/legate/web/views/agent.slim +436 -0
- data/lib/legate/web/views/agent_auth.slim +562 -0
- data/lib/legate/web/views/agents.slim +369 -0
- data/lib/legate/web/views/auth.slim +112 -0
- data/lib/legate/web/views/auth_credential_detail.slim +327 -0
- data/lib/legate/web/views/auth_credentials.slim +261 -0
- data/lib/legate/web/views/auth_debug.slim +94 -0
- data/lib/legate/web/views/auth_mapping_detail.slim +151 -0
- data/lib/legate/web/views/auth_mapping_new.slim +123 -0
- data/lib/legate/web/views/auth_mappings.slim +120 -0
- data/lib/legate/web/views/auth_scheme_detail.slim +274 -0
- data/lib/legate/web/views/auth_schemes.slim +259 -0
- data/lib/legate/web/views/auth_test.slim +418 -0
- data/lib/legate/web/views/chat.slim +192 -0
- data/lib/legate/web/views/docs_index.slim +105 -0
- data/lib/legate/web/views/docs_show.slim +105 -0
- data/lib/legate/web/views/error_404.slim +5 -0
- data/lib/legate/web/views/index.slim +148 -0
- data/lib/legate/web/views/layout.slim +144 -0
- data/lib/legate/web/views/tool_detail.slim +87 -0
- data/lib/legate/web/views/tools.slim +50 -0
- data/lib/legate/web/webhook_listener.rb +367 -0
- data/lib/legate/web.rb +9 -0
- data/lib/legate.rb +220 -0
- data/public/docs/advanced/callbacks.md +828 -0
- data/public/docs/advanced/mcp_schema_conversion.md +59 -0
- data/public/docs/authentication/api_reference/config.md +210 -0
- data/public/docs/authentication/api_reference/credential.md +246 -0
- data/public/docs/authentication/api_reference/encryption.md +218 -0
- data/public/docs/authentication/api_reference/exchanged_credential.md +271 -0
- data/public/docs/authentication/api_reference/excon_middleware.md +175 -0
- data/public/docs/authentication/api_reference/index.md +30 -0
- data/public/docs/authentication/api_reference/scheme.md +250 -0
- data/public/docs/authentication/api_reference/schemes/api_key.md +175 -0
- data/public/docs/authentication/api_reference/schemes/google_service_account.md +221 -0
- data/public/docs/authentication/api_reference/schemes/http_bearer.md +169 -0
- data/public/docs/authentication/api_reference/schemes/oauth2.md +343 -0
- data/public/docs/authentication/api_reference/schemes/oidc.md +73 -0
- data/public/docs/authentication/api_reference/schemes/openid_connect.md +311 -0
- data/public/docs/authentication/api_reference/schemes/service_account.md +287 -0
- data/public/docs/authentication/api_reference/token_manager.md +221 -0
- data/public/docs/authentication/api_reference/token_store.md +146 -0
- data/public/docs/authentication/api_reference/tool_context_extension.md +166 -0
- data/public/docs/authentication/guides/api_key.md +190 -0
- data/public/docs/authentication/guides/bearer.md +172 -0
- data/public/docs/authentication/guides/configuration.md +255 -0
- data/public/docs/authentication/guides/custom_flow.md +523 -0
- data/public/docs/authentication/guides/index.md +24 -0
- data/public/docs/authentication/guides/migration.md +435 -0
- data/public/docs/authentication/guides/oauth2.md +252 -0
- data/public/docs/authentication/guides/oidc.md +241 -0
- data/public/docs/authentication/guides/overview.md +155 -0
- data/public/docs/authentication/guides/secure_storage.md +301 -0
- data/public/docs/authentication/guides/service_account.md +228 -0
- data/public/docs/authentication/guides/token_lifecycle.md +295 -0
- data/public/docs/authentication/guides/web_ui_integration.md +504 -0
- data/public/docs/authentication/index.md +58 -0
- data/public/docs/authentication/troubleshooting/credential_storage.md +550 -0
- data/public/docs/authentication/troubleshooting/environment_variables.md +540 -0
- data/public/docs/authentication/troubleshooting/index.md +11 -0
- data/public/docs/authentication/troubleshooting/oauth2_issues.md +220 -0
- data/public/docs/authentication/troubleshooting/oidc_issues.md +412 -0
- data/public/docs/authentication/troubleshooting/token_refresh.md +338 -0
- data/public/docs/cli/legate_cli_usage.md +363 -0
- data/public/docs/core_concepts/legate_agent_lifecycle.md +124 -0
- data/public/docs/core_concepts/legate_architecture_overview.md +110 -0
- data/public/docs/core_concepts/legate_configuration.md +116 -0
- data/public/docs/core_concepts/legate_definition_store.md +102 -0
- data/public/docs/core_concepts/legate_planner.md +94 -0
- data/public/docs/core_concepts/legate_session_service.md +104 -0
- data/public/docs/error_handling/legate_error_handling.md +122 -0
- data/public/docs/examples.md +199 -0
- data/public/docs/getting_started.md +111 -0
- data/public/docs/guides/agentic_agents.md +137 -0
- data/public/docs/guides/ai_code_generators.md +437 -0
- data/public/docs/guides/auto_loading.md +326 -0
- data/public/docs/guides/configuring_agent_webhooks.md +219 -0
- data/public/docs/guides/http_client_usage.md +264 -0
- data/public/docs/guides/llm_providers.md +137 -0
- data/public/docs/guides/mcp_client_integration.md +232 -0
- data/public/docs/guides/mcp_server_exposure.md +206 -0
- data/public/docs/guides/rails_integration.md +128 -0
- data/public/docs/guides/sending_outbound_webhooks.md +227 -0
- data/public/docs/guides/streaming.md +112 -0
- data/public/docs/guides/webhooks.md +288 -0
- data/public/docs/introduction.md +51 -0
- data/public/docs/multi_agent_systems/advanced_features.md +57 -0
- data/public/docs/multi_agent_systems/agent_delegation.md +190 -0
- data/public/docs/multi_agent_systems/agent_hierarchy.md +49 -0
- data/public/docs/multi_agent_systems/state_management.md +47 -0
- data/public/docs/multi_agent_systems/workflow_agents.md +72 -0
- data/public/docs/tools/legate_built_in_tools.md +332 -0
- data/public/docs/tools/legate_tools_and_registry.md +263 -0
- data/public/docs/web_ui/legate_web_ui.md +137 -0
- 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'
|