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,187 @@
|
|
|
1
|
+
# File: lib/legate/tools/agent_tool.rb
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative '../tool'
|
|
5
|
+
require_relative '../agent'
|
|
6
|
+
# ToolRegistry is NOT needed directly by AgentTool for loading target tools
|
|
7
|
+
# require_relative '../tool_registry'
|
|
8
|
+
require_relative '../session'
|
|
9
|
+
require_relative '../session_service/in_memory'
|
|
10
|
+
require 'json'
|
|
11
|
+
require 'securerandom'
|
|
12
|
+
require_relative '../global_definition_registry'
|
|
13
|
+
require_relative '../global_tool_manager' # Make sure this is required
|
|
14
|
+
|
|
15
|
+
module Legate
|
|
16
|
+
module Tools
|
|
17
|
+
class AgentTool < Legate::Tool
|
|
18
|
+
self.explicit_tool_name = :delegate_task
|
|
19
|
+
|
|
20
|
+
tool_description 'Delegates a specified task to another agent identified by its unique name. Use this when a specific agent is better suited for the sub-task.'
|
|
21
|
+
|
|
22
|
+
parameter :target_agent_name,
|
|
23
|
+
type: :string,
|
|
24
|
+
description: 'The unique name of the agent definition to delegate the task to.',
|
|
25
|
+
required: true
|
|
26
|
+
|
|
27
|
+
parameter :task,
|
|
28
|
+
type: :string,
|
|
29
|
+
description: 'The specific task description to be executed by the target agent.',
|
|
30
|
+
required: true
|
|
31
|
+
|
|
32
|
+
parameter :use_calling_session,
|
|
33
|
+
type: :boolean,
|
|
34
|
+
description: 'If true, the target agent executes within the same session context as the caller. If false, a new isolated session is created. Defaults to false.',
|
|
35
|
+
required: false
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def perform_execution(params, context) # context is the ToolContext of the calling agent
|
|
40
|
+
# Validate that the context has a valid tool registry
|
|
41
|
+
unless context && context.respond_to?(:tool_registry) && context.tool_registry
|
|
42
|
+
msg = 'Tool registry not found or invalid in the provided context'
|
|
43
|
+
Legate.logger.error("AgentTool: #{msg}")
|
|
44
|
+
raise Legate::ToolError, msg
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
target_agent_name_str = params.fetch(:target_agent_name) do
|
|
48
|
+
raise Legate::ToolArgumentError, 'Missing required parameter: target_agent_name'
|
|
49
|
+
end.to_s # Ensure it's a string for store lookup
|
|
50
|
+
|
|
51
|
+
task_to_delegate = params.fetch(:task) { raise Legate::ToolArgumentError, 'Missing required parameter: task' }
|
|
52
|
+
use_calling_session = params.fetch(:use_calling_session, false)
|
|
53
|
+
|
|
54
|
+
Legate.logger.info("AgentTool: Attempting to delegate task '#{task_to_delegate}' to agent '#{target_agent_name_str}' (Session reuse: #{use_calling_session})")
|
|
55
|
+
|
|
56
|
+
# Load definition from the GlobalDefinitionRegistry
|
|
57
|
+
agent_definition_object = Legate::GlobalDefinitionRegistry.find(target_agent_name_str.to_sym)
|
|
58
|
+
|
|
59
|
+
# If not found as AgentDefinition, try getting as hash
|
|
60
|
+
definition_hash = (Legate::GlobalDefinitionRegistry.get_definition(target_agent_name_str.to_sym) if agent_definition_object)
|
|
61
|
+
|
|
62
|
+
unless definition_hash
|
|
63
|
+
msg = "Target agent definition '#{target_agent_name_str}' could not be loaded from registry."
|
|
64
|
+
Legate.logger.error("AgentTool: #{msg}")
|
|
65
|
+
raise Legate::ToolArgumentError, msg
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Ensure we have essential fields for a valid definition
|
|
69
|
+
definition_hash = definition_hash.transform_keys(&:to_sym) if definition_hash.respond_to?(:transform_keys)
|
|
70
|
+
|
|
71
|
+
# Ensure the definition has all required fields
|
|
72
|
+
definition_hash[:name] = target_agent_name_str.to_sym unless definition_hash.key?(:name)
|
|
73
|
+
definition_hash[:description] = definition_hash[:description] || "Delegated agent #{target_agent_name_str}"
|
|
74
|
+
definition_hash[:instruction] = definition_hash[:instruction] || "Perform the delegated task: #{task_to_delegate}"
|
|
75
|
+
|
|
76
|
+
# Handle 'tools' field: parse if JSON string, convert to array of symbols
|
|
77
|
+
if definition_hash.key?(:tools)
|
|
78
|
+
tool_array = if definition_hash[:tools].is_a?(String)
|
|
79
|
+
begin
|
|
80
|
+
parsed = JSON.parse(definition_hash[:tools])
|
|
81
|
+
parsed.is_a?(Array) ? parsed : []
|
|
82
|
+
rescue JSON::ParserError
|
|
83
|
+
Legate.logger.warn("AgentTool: Could not parse :tools JSON for agent '#{target_agent_name_str}'. Defaulting to empty tools array.")
|
|
84
|
+
[]
|
|
85
|
+
end
|
|
86
|
+
else
|
|
87
|
+
Array(definition_hash[:tools])
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Convert to symbols for the definition
|
|
91
|
+
definition_hash[:tools] = tool_array.map(&:to_sym)
|
|
92
|
+
elsif !definition_hash.key?(:tool_names) # Ensure some form of tools field exists
|
|
93
|
+
definition_hash[:tools] = []
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Handle 'mcp_servers_json' field: if present and no mcp_servers, rename
|
|
97
|
+
definition_hash[:mcp_servers] = definition_hash.delete(:mcp_servers_json) if definition_hash.key?(:mcp_servers_json) && !definition_hash.key?(:mcp_servers)
|
|
98
|
+
|
|
99
|
+
# Ensure fallback_mode is symbolized
|
|
100
|
+
definition_hash[:fallback_mode] = definition_hash[:fallback_mode].to_sym if definition_hash[:fallback_mode].is_a?(String)
|
|
101
|
+
|
|
102
|
+
# Convert hash to an Legate::AgentDefinition object
|
|
103
|
+
target_definition_object = Legate::AgentDefinition.from_hash(definition_hash)
|
|
104
|
+
|
|
105
|
+
unless target_definition_object
|
|
106
|
+
msg = "Failed to create a valid AgentDefinition object for target '#{target_agent_name_str}' from loaded hash."
|
|
107
|
+
Legate.logger.error("AgentTool: #{msg} Hash was: #{definition_hash.inspect}")
|
|
108
|
+
raise Legate::ToolError, msg # More generic error as it's post-load
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
Legate.logger.debug("AgentTool: Instantiating target agent '#{target_definition_object.name}' using its definition object.")
|
|
112
|
+
|
|
113
|
+
# Determine session service and session ID to use
|
|
114
|
+
delegate_session_service = nil
|
|
115
|
+
delegate_session_id = nil
|
|
116
|
+
|
|
117
|
+
if use_calling_session
|
|
118
|
+
if context.session_service
|
|
119
|
+
delegate_session_service = context.session_service
|
|
120
|
+
delegate_session_id = context.session_id
|
|
121
|
+
Legate.logger.debug("AgentTool: Reusing session service and session ID '#{delegate_session_id}' from caller.")
|
|
122
|
+
else
|
|
123
|
+
Legate.logger.warn('AgentTool: use_calling_session is true but context has no session_service. Falling back to new isolated session.')
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Fallback if reuse failed or not requested
|
|
128
|
+
unless delegate_session_service
|
|
129
|
+
delegate_session_service = Legate::SessionService::InMemory.new
|
|
130
|
+
# Create a new session
|
|
131
|
+
new_session = delegate_session_service.create_session(
|
|
132
|
+
app_name: target_definition_object.name.to_s,
|
|
133
|
+
user_id: "delegation_#{SecureRandom.hex(4)}"
|
|
134
|
+
)
|
|
135
|
+
delegate_session_id = new_session.id
|
|
136
|
+
Legate.logger.debug("AgentTool: Created new isolated session '#{delegate_session_id}' for target agent.")
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Create the ephemeral agent using its definition object
|
|
140
|
+
target_agent = Legate::Agent.new(
|
|
141
|
+
definition: target_definition_object,
|
|
142
|
+
session_service: delegate_session_service
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
# Check if tools are configured for this agent
|
|
146
|
+
Legate.logger.warn("AgentTool: Target agent '#{target_agent_name_str}' has no tools configured.") if target_definition_object.tool_names.empty?
|
|
147
|
+
|
|
148
|
+
# Register tools - get the class objects for each tool name and register with the agent
|
|
149
|
+
tool_names = Array(definition_hash[:tools] || definition_hash[:tool_names] || [])
|
|
150
|
+
tool_names.each do |tool_name|
|
|
151
|
+
tool_class = Legate::GlobalToolManager.find_class(tool_name.to_sym)
|
|
152
|
+
if tool_class
|
|
153
|
+
target_agent.register_tool_class(tool_class)
|
|
154
|
+
Legate.logger.debug("AgentTool: Registered tool '#{tool_name}' with target agent.")
|
|
155
|
+
else
|
|
156
|
+
Legate.logger.warn("AgentTool: Could not find tool class for '#{tool_name}' in GlobalToolManager.")
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
target_agent.start
|
|
161
|
+
Legate.logger.info("AgentTool: Running task '#{task_to_delegate}' on target agent '#{target_agent.name}' (Session: #{delegate_session_id})")
|
|
162
|
+
|
|
163
|
+
agent_event = target_agent.run_task(
|
|
164
|
+
session_id: delegate_session_id,
|
|
165
|
+
user_input: task_to_delegate,
|
|
166
|
+
session_service: delegate_session_service # Pass the correct service
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
target_result = agent_event.respond_to?(:content) ? agent_event.content : agent_event
|
|
170
|
+
Legate.logger.info("AgentTool: Target agent '#{target_agent.name}' finished task. Result: #{target_result.inspect}")
|
|
171
|
+
|
|
172
|
+
{ status: :success, result: target_result }
|
|
173
|
+
rescue Legate::ToolArgumentError => e
|
|
174
|
+
Legate.logger.error("AgentTool ArgumentError: #{e.message}")
|
|
175
|
+
raise e
|
|
176
|
+
rescue Legate::ToolError => e
|
|
177
|
+
Legate.logger.error("AgentTool ToolError: #{e.message}")
|
|
178
|
+
raise e
|
|
179
|
+
rescue StandardError => e
|
|
180
|
+
msg = "AgentTool: Unexpected error during delegation to '#{target_agent_name_str}': #{e.class} - #{e.message}"
|
|
181
|
+
Legate.logger.error(msg)
|
|
182
|
+
Legate.logger.error(e.backtrace.first(5).join("\n"))
|
|
183
|
+
raise Legate::ToolError, msg
|
|
184
|
+
end # end perform_execution
|
|
185
|
+
end # End AgentTool class
|
|
186
|
+
end # End Tools module
|
|
187
|
+
end # End Legate module
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
# File: lib/legate/tools/base/http_client.rb
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'excon'
|
|
5
|
+
require 'json'
|
|
6
|
+
require 'uri' # For URI.join
|
|
7
|
+
|
|
8
|
+
# Tool errors are defined in legate/errors.rb (loaded by lib/legate.rb)
|
|
9
|
+
require_relative '../../version'
|
|
10
|
+
|
|
11
|
+
module Legate
|
|
12
|
+
module Tools
|
|
13
|
+
module Base
|
|
14
|
+
# Mixin module providing standardized, reusable HTTP client capabilities
|
|
15
|
+
# for Legate tools, built upon the Excon gem.
|
|
16
|
+
#
|
|
17
|
+
# Include this module in your Tool class and call `setup_http_client`
|
|
18
|
+
# in your `initialize` method.
|
|
19
|
+
#
|
|
20
|
+
# It offers helper methods (http_get, http_head, http_post, etc.) for common requests,
|
|
21
|
+
# handles base URL joining, JSON encoding/decoding (optional), logging,
|
|
22
|
+
# and wraps Excon errors into standardized Legate::ToolError subclasses.
|
|
23
|
+
module HttpClient
|
|
24
|
+
# Custom instrumentor that only logs errors
|
|
25
|
+
class QuietInstrumentor < Excon::StandardInstrumentor
|
|
26
|
+
def instrument(name, params = {})
|
|
27
|
+
# Only log if there's an error
|
|
28
|
+
Legate.logger.error("[#{name}] #{params[:error]}") if params[:error]
|
|
29
|
+
yield if block_given?
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
attr_reader :http_client, :http_base_url
|
|
34
|
+
|
|
35
|
+
# Make request helpers public API for tools including this module
|
|
36
|
+
|
|
37
|
+
def http_get(path, query: {}, headers: {}, options: {})
|
|
38
|
+
make_request(:get, path, query: query, headers: headers, options: options)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def http_head(path, query: {}, headers: {}, options: {})
|
|
42
|
+
make_request(:head, path, query: query, headers: headers, options: options)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def http_post(path, body: nil, query: {}, headers: {}, options: {})
|
|
46
|
+
make_request(:post, path, body: body, query: query, headers: headers, options: options)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def http_put(path, body: nil, query: {}, headers: {}, options: {})
|
|
50
|
+
make_request(:put, path, body: body, query: query, headers: headers, options: options)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def http_delete(path, query: {}, headers: {}, options: {})
|
|
54
|
+
make_request(:delete, path, query: query, headers: headers, options: options)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
protected
|
|
58
|
+
|
|
59
|
+
# Initializes the Excon HTTP client instance.
|
|
60
|
+
# Should be called by the including tool, typically in its `initialize` method.
|
|
61
|
+
# Stores the connection instance in `@http_client` and base URL in `@http_base_url`.
|
|
62
|
+
#
|
|
63
|
+
# @param base_url [String] The base URL for the API. Must be a valid URI string.
|
|
64
|
+
# @param headers [Hash] Default headers to include in every request (merged with defaults).
|
|
65
|
+
# @param options [Hash] Options passed directly to `Excon.new`. See Excon documentation for available options
|
|
66
|
+
# (e.g., :read_timeout, :write_timeout, :connect_timeout, :persistent, :proxy, :ssl_verify_peer).
|
|
67
|
+
# Allows overriding the default instrumentor via `:instrumentor` key.
|
|
68
|
+
# @return [void]
|
|
69
|
+
# @raise [Legate::ToolError] If the base_url is invalid or Excon initialization fails.
|
|
70
|
+
def setup_http_client(base_url:, headers: {}, options: {})
|
|
71
|
+
# Revert: base_url is required again
|
|
72
|
+
begin
|
|
73
|
+
@http_base_url = URI.parse(base_url.to_s)
|
|
74
|
+
raise URI::InvalidURIError, 'Scheme must be http or https' unless @http_base_url.is_a?(URI::HTTP) || @http_base_url.is_a?(URI::HTTPS)
|
|
75
|
+
rescue URI::InvalidURIError => e
|
|
76
|
+
raise Legate::ToolError.new("Invalid base_url provided: #{base_url} - #{e.message}", cause: e)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
default_user_agent = "Legate-Ruby/#{Legate::VERSION} #{Excon::USER_AGENT}"
|
|
80
|
+
default_headers = { 'User-Agent' => default_user_agent }
|
|
81
|
+
merged_headers = default_headers.merge(headers)
|
|
82
|
+
default_options = { persistent: true, connect_timeout: 5, read_timeout: 15, write_timeout: 15,
|
|
83
|
+
instrumentor: QuietInstrumentor.new }
|
|
84
|
+
final_options = default_options.merge(options)
|
|
85
|
+
final_options[:headers] = merged_headers unless merged_headers.empty?
|
|
86
|
+
|
|
87
|
+
# Store connection options for potential use in make_request for absolute URLs
|
|
88
|
+
@http_connection_options = final_options.dup
|
|
89
|
+
|
|
90
|
+
begin
|
|
91
|
+
log_options_for_debug = final_options.dup
|
|
92
|
+
log_options_for_debug[:headers] = '[REDACTED]' unless Legate.logger.level == Logger::DEBUG
|
|
93
|
+
Legate.logger.debug("Setting up Excon client for #{self.class.name} with base URL: #{@http_base_url}")
|
|
94
|
+
Legate.logger.debug("Excon options: #{log_options_for_debug}")
|
|
95
|
+
|
|
96
|
+
# Create connection using the (mandatory) base URL
|
|
97
|
+
@http_client = Excon.new(@http_base_url.to_s, final_options)
|
|
98
|
+
|
|
99
|
+
# Store default *request* options and headers separately
|
|
100
|
+
@http_default_request_options = final_options.reject { |k, _|
|
|
101
|
+
%i[headers instrumentor instrumentor_params].include?(k)
|
|
102
|
+
}
|
|
103
|
+
@http_default_headers = merged_headers
|
|
104
|
+
rescue Excon::Error::Socket => e
|
|
105
|
+
err_msg = "Failed to initialize Excon connection for #{self.class.name} to #{@http_base_url}: #{e.message}"
|
|
106
|
+
Legate.logger.error(err_msg)
|
|
107
|
+
@http_client = nil
|
|
108
|
+
raise Legate::ToolNetworkError.new(err_msg, cause: e)
|
|
109
|
+
rescue StandardError => e
|
|
110
|
+
err_msg = "Unexpected error initializing Excon for #{self.class.name}: #{e.class} - #{e.message}"
|
|
111
|
+
Legate.logger.error(err_msg)
|
|
112
|
+
@http_client = nil
|
|
113
|
+
raise Legate::ToolError.new(err_msg, cause: e)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# TODO: Add any private helper methods if needed
|
|
118
|
+
|
|
119
|
+
private
|
|
120
|
+
|
|
121
|
+
def resolve_target_uri(path)
|
|
122
|
+
parsed_path = URI.parse(path.to_s)
|
|
123
|
+
if parsed_path.is_a?(URI::HTTP) || parsed_path.is_a?(URI::HTTPS)
|
|
124
|
+
[parsed_path, true]
|
|
125
|
+
else
|
|
126
|
+
[URI.join(@http_base_url, path), false]
|
|
127
|
+
end
|
|
128
|
+
rescue URI::InvalidURIError => e
|
|
129
|
+
raise Legate::ToolError.new("Invalid URL or path provided: #{path} - #{e.message}", cause: e)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Centralized method for making HTTP requests and handling common errors/wrapping.
|
|
133
|
+
def make_request(method, path, body: nil, query: {}, headers: {}, options: {})
|
|
134
|
+
# Extract SSRF protection options before merging into Excon params
|
|
135
|
+
resolved_ip = options.delete(:resolved_ip)
|
|
136
|
+
original_host = options.delete(:original_host)
|
|
137
|
+
|
|
138
|
+
# Ensure setup was called, but @http_client might not be used if path is absolute
|
|
139
|
+
unless @http_connection_options
|
|
140
|
+
raise Legate::ToolError,
|
|
141
|
+
'HTTP client options not initialized. Call setup_http_client first.'
|
|
142
|
+
end
|
|
143
|
+
raise Legate::ToolError, 'Base URL not set properly during setup.' unless @http_base_url
|
|
144
|
+
|
|
145
|
+
request_params = @http_default_request_options.merge(options)
|
|
146
|
+
request_params[:method] = method
|
|
147
|
+
|
|
148
|
+
target_uri, is_absolute = resolve_target_uri(path)
|
|
149
|
+
|
|
150
|
+
# Path/Query setup differs slightly for absolute vs relative
|
|
151
|
+
if is_absolute
|
|
152
|
+
# For absolute URLs, the full path/query is part of target_uri
|
|
153
|
+
# We don't need to set host/scheme/port in request_params
|
|
154
|
+
# as Excon.new will use the full target_uri.to_s
|
|
155
|
+
else
|
|
156
|
+
# For relative URLs, use the persistent client and set path/query
|
|
157
|
+
end
|
|
158
|
+
request_params[:path] = target_uri.request_uri
|
|
159
|
+
|
|
160
|
+
# Merge explicit query params with any existing in the URI
|
|
161
|
+
uri_query = URI.decode_www_form(target_uri.query || '').to_h
|
|
162
|
+
final_query = uri_query.merge(query)
|
|
163
|
+
request_params[:query] = final_query unless final_query.empty?
|
|
164
|
+
# Update path if query was added/changed (remove original query part if exists)
|
|
165
|
+
request_params[:path] = target_uri.path
|
|
166
|
+
|
|
167
|
+
# 3. Merge Headers
|
|
168
|
+
request_params[:headers] = @http_default_headers.merge(headers)
|
|
169
|
+
|
|
170
|
+
# Determine if Content-Type was explicitly passed
|
|
171
|
+
custom_content_type_provided = headers.keys.any? { |k| k.to_s.casecmp('Content-Type').zero? }
|
|
172
|
+
content_type_key = request_params[:headers].keys.find { |k|
|
|
173
|
+
k.to_s.casecmp('Content-Type').zero?
|
|
174
|
+
} || 'Content-Type'
|
|
175
|
+
|
|
176
|
+
# 4. Handle Request Body and Content-Type logic
|
|
177
|
+
if body.is_a?(Hash) && %i[post put patch].include?(method)
|
|
178
|
+
# Only default to application/json if Content-Type was not explicitly provided
|
|
179
|
+
request_params[:headers][content_type_key] = 'application/json; charset=utf-8' unless custom_content_type_provided
|
|
180
|
+
# Get the final effective content type for encoding check
|
|
181
|
+
final_content_type = request_params[:headers].find { |k, _| k.to_s.casecmp('Content-Type').zero? }&.last
|
|
182
|
+
|
|
183
|
+
if final_content_type&.start_with?('application/json')
|
|
184
|
+
# ... JSON encode body ...
|
|
185
|
+
begin
|
|
186
|
+
request_params[:body] = JSON.generate(body)
|
|
187
|
+
rescue JSON::GeneratorError => e
|
|
188
|
+
# raise Legate::ToolError, "Failed to encode request body as JSON: #{e.message}" # No cause
|
|
189
|
+
# Add cause for better debugging
|
|
190
|
+
raise Legate::ToolError.new("Failed to encode request body as JSON: #{e.message}", cause: e)
|
|
191
|
+
end
|
|
192
|
+
else
|
|
193
|
+
# ... Handle Hash body with non-JSON CT ...
|
|
194
|
+
Legate.logger.warn "Sending Hash body with non-JSON Content-Type (#{final_content_type}) for #{target_uri}"
|
|
195
|
+
request_params[:body] = body
|
|
196
|
+
end
|
|
197
|
+
elsif body # Body is not a Hash (likely a String)
|
|
198
|
+
request_params[:body] = body
|
|
199
|
+
# If body is string AND Content-Type wasn't explicitly passed, remove the default one.
|
|
200
|
+
unless custom_content_type_provided
|
|
201
|
+
key_to_delete = request_params[:headers].keys.find { |k| k.to_s.casecmp('Content-Type').zero? }
|
|
202
|
+
request_params[:headers].delete(key_to_delete) if key_to_delete
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# 5. Execute Request: Choose client based on absolute vs relative path
|
|
207
|
+
Legate.logger.info "Executing HTTP #{method.to_s.upcase} request to #{target_uri}"
|
|
208
|
+
|
|
209
|
+
response = nil
|
|
210
|
+
if is_absolute
|
|
211
|
+
Legate.logger.debug "Using temporary Excon client for absolute URL: #{target_uri}"
|
|
212
|
+
|
|
213
|
+
# Prepare options for the temporary Excon client instance
|
|
214
|
+
temp_client_options = @http_connection_options.reject { |k, _| k == :headers }
|
|
215
|
+
# Deep duplicate headers hash to avoid modifying the original
|
|
216
|
+
final_headers_for_new = Marshal.load(Marshal.dump(@http_connection_options[:headers] || {}))
|
|
217
|
+
# Merge the fully processed request_params[:headers] (which includes defaults and customs)
|
|
218
|
+
final_headers_for_new.merge!(request_params[:headers].transform_keys(&:to_s))
|
|
219
|
+
|
|
220
|
+
# DNS pinning: connect to pre-resolved IP to prevent DNS rebinding (TOCTOU)
|
|
221
|
+
connect_uri = target_uri
|
|
222
|
+
if resolved_ip
|
|
223
|
+
connect_uri = target_uri.dup
|
|
224
|
+
# #hostname= brackets IPv6 literals so the URI stays valid.
|
|
225
|
+
connect_uri.hostname = resolved_ip
|
|
226
|
+
host_for_tls = original_host || target_uri.host
|
|
227
|
+
final_headers_for_new['Host'] ||= host_for_tls
|
|
228
|
+
if target_uri.scheme == 'https'
|
|
229
|
+
# Use the real hostname for SNI and certificate verification even
|
|
230
|
+
# though we connect to the pinned IP (otherwise the cert check
|
|
231
|
+
# compares against the IP and every HTTPS request fails).
|
|
232
|
+
temp_client_options[:hostname] = host_for_tls
|
|
233
|
+
temp_client_options[:ssl_verify_peer_host] = host_for_tls
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
temp_client_options[:headers] = final_headers_for_new
|
|
238
|
+
|
|
239
|
+
temp_client = Excon.new(connect_uri.to_s, temp_client_options)
|
|
240
|
+
|
|
241
|
+
# Prepare the params for the .request call (method, body, query, etc., NO headers)
|
|
242
|
+
request_params_for_absolute = request_params.reject { |k, _| k == :headers }
|
|
243
|
+
|
|
244
|
+
Legate.logger.debug "Excon Temp Request Params (for .request call): #{request_params_for_absolute.inspect}"
|
|
245
|
+
Legate.logger.debug "Excon Temp Client Options (for .new call): #{temp_client_options.inspect}"
|
|
246
|
+
response = temp_client.request(request_params_for_absolute)
|
|
247
|
+
else
|
|
248
|
+
Legate.logger.debug "Using persistent Excon client for relative path: #{target_uri}"
|
|
249
|
+
# Use the persistent client setup with the base URL
|
|
250
|
+
raise Legate::ToolError, 'Persistent HTTP client not initialized.' unless @http_client
|
|
251
|
+
|
|
252
|
+
Legate.logger.debug "Excon Persistent Request Params: #{request_params.inspect}"
|
|
253
|
+
response = @http_client.request(request_params)
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
Legate.logger.info "Received HTTP response: Status #{response.status}"
|
|
257
|
+
Legate.logger.debug "Response Body: #{response.body[0..500]}..."
|
|
258
|
+
|
|
259
|
+
unless (200..299).cover?(response.status)
|
|
260
|
+
err_msg = "HTTP Error: Received status #{response.status} for #{method.to_s.upcase} #{target_uri}"
|
|
261
|
+
Legate.logger.error(err_msg)
|
|
262
|
+
raise Excon::Error::HTTPStatus.new(err_msg, nil, response)
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
response
|
|
266
|
+
|
|
267
|
+
# Step 7: Error Wrapping Logic
|
|
268
|
+
rescue Excon::Error::Timeout => e
|
|
269
|
+
err_msg = "Timeout during #{method.to_s.upcase} request to #{target_uri || path}: #{e.message}"
|
|
270
|
+
Legate.logger.error(err_msg)
|
|
271
|
+
raise Legate::ToolTimeoutError.new(err_msg, cause: e)
|
|
272
|
+
rescue Excon::Error::Socket => e
|
|
273
|
+
err_msg = "Network/Socket error during #{method.to_s.upcase} request to #{target_uri || path}: #{e.message}"
|
|
274
|
+
Legate.logger.error(err_msg)
|
|
275
|
+
raise Legate::ToolNetworkError.new(err_msg, cause: e)
|
|
276
|
+
rescue Excon::Error::Certificate => e
|
|
277
|
+
err_msg = "SSL Certificate error during #{method.to_s.upcase} request to #{target_uri || path}: #{e.message}"
|
|
278
|
+
Legate.logger.error(err_msg)
|
|
279
|
+
raise Legate::ToolCertificateError.new(err_msg, cause: e)
|
|
280
|
+
rescue Excon::Error::HTTPStatus => e
|
|
281
|
+
status = e.response&.status || 'N/A'
|
|
282
|
+
body_preview = e.response&.body&.slice(0, 500)
|
|
283
|
+
err_msg = "HTTP Error: Received status #{status} for #{method.to_s.upcase} #{target_uri || path}"
|
|
284
|
+
Legate.logger.error("#{err_msg} - Response Body: #{body_preview}...")
|
|
285
|
+
raise Legate::ToolHttpError.new(err_msg, response: e.response, cause: e)
|
|
286
|
+
rescue Excon::Error => e
|
|
287
|
+
status = e.respond_to?(:response) && e.response ? e.response.status : 'N/A'
|
|
288
|
+
err_msg = "Excon error during #{method.to_s.upcase} request to #{target_uri || path} (Status: #{status}): #{e.class} - #{e.message}"
|
|
289
|
+
Legate.logger.error(err_msg)
|
|
290
|
+
raise Legate::ToolError.new(err_msg, cause: e)
|
|
291
|
+
# Catch Legate::ToolError explicitly first to prevent re-wrapping
|
|
292
|
+
rescue Legate::ToolError => e
|
|
293
|
+
raise e
|
|
294
|
+
# Catch StandardError last, covering errors during setup (like URI.join, JSON.generate if not caught above)
|
|
295
|
+
rescue StandardError => e
|
|
296
|
+
# Avoid re-wrapping Legate::ToolErrors that might bubble up (e.g., from URI.join failure)
|
|
297
|
+
# raise if e.is_a?(Legate::ToolError) # Handled by the rescue above now
|
|
298
|
+
|
|
299
|
+
# Make error message generation safer but include original message
|
|
300
|
+
error_class_name = begin
|
|
301
|
+
e.class.name
|
|
302
|
+
rescue StandardError
|
|
303
|
+
'UnknownError'
|
|
304
|
+
end
|
|
305
|
+
error_message = begin
|
|
306
|
+
e.message
|
|
307
|
+
rescue StandardError
|
|
308
|
+
'No message available'
|
|
309
|
+
end
|
|
310
|
+
err_msg = "Unexpected error during #{method.to_s.upcase} request logic: #{error_class_name} - #{error_message}"
|
|
311
|
+
Legate.logger.error(err_msg)
|
|
312
|
+
# Safely log backtrace if available - REMOVED as it might cause issues with already wrapped Legate::ToolErrors
|
|
313
|
+
# Raise without cause for StandardError as it can cause issues
|
|
314
|
+
raise Legate::ToolError, err_msg
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
end
|
|
319
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# File: lib/legate/tools/base/safe_url.rb
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'uri'
|
|
5
|
+
require 'ipaddr'
|
|
6
|
+
require_relative '../../auth/url_guard'
|
|
7
|
+
require_relative '../../errors'
|
|
8
|
+
|
|
9
|
+
module Legate
|
|
10
|
+
module Tools
|
|
11
|
+
module Base
|
|
12
|
+
# SSRF guard for outbound tool requests.
|
|
13
|
+
#
|
|
14
|
+
# Validates a URL and returns the IP to pin the connection to (defeating
|
|
15
|
+
# DNS-rebinding TOCTOU). It reuses the canonical {Legate::Auth::UrlGuard}
|
|
16
|
+
# block-list so tools and the auth layer can never drift out of sync, and
|
|
17
|
+
# raises a tool-appropriate {Legate::ToolArgumentError} on a bad target.
|
|
18
|
+
#
|
|
19
|
+
# Set LEGATE_ALLOW_PRIVATE_TOOL_URLS=1 to reach private/loopback hosts in
|
|
20
|
+
# development (returns no pin so the request connects directly).
|
|
21
|
+
module SafeUrl
|
|
22
|
+
module_function
|
|
23
|
+
|
|
24
|
+
# @param url [String] the target URL
|
|
25
|
+
# @return [Array(URI, String|nil)] the parsed URI and the IP to pin to
|
|
26
|
+
# (nil when the dev bypass is active)
|
|
27
|
+
# @raise [Legate::ToolArgumentError] if the URL is not http(s), cannot be
|
|
28
|
+
# resolved, or resolves to a restricted (loopback / private / link-local
|
|
29
|
+
# / CGNAT / 0.0.0.0-8) address
|
|
30
|
+
def resolve!(url)
|
|
31
|
+
uri = URI.parse(url.to_s)
|
|
32
|
+
raise Legate::ToolArgumentError, "URL must use http or https: #{url}" unless uri.is_a?(URI::HTTP) || uri.is_a?(URI::HTTPS)
|
|
33
|
+
|
|
34
|
+
return [uri, nil] if ENV['LEGATE_ALLOW_PRIVATE_TOOL_URLS']
|
|
35
|
+
|
|
36
|
+
ips = Legate::Auth::UrlGuard.resolved_ips(uri.host)
|
|
37
|
+
raise Legate::ToolArgumentError, "Could not resolve host: #{uri.host}" if ips.empty?
|
|
38
|
+
|
|
39
|
+
ips.each do |ip_str|
|
|
40
|
+
ip = IPAddr.new(ip_str)
|
|
41
|
+
next unless Legate::Auth::UrlGuard.restricted?(ip)
|
|
42
|
+
|
|
43
|
+
raise Legate::ToolArgumentError,
|
|
44
|
+
"Blocked request to restricted network address (#{uri.host} -> #{ip_str})"
|
|
45
|
+
rescue IPAddr::InvalidAddressError
|
|
46
|
+
raise Legate::ToolArgumentError, "Invalid IP resolved for #{uri.host}: #{ip_str}"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
[uri, ips.first]
|
|
50
|
+
rescue URI::InvalidURIError => e
|
|
51
|
+
raise Legate::ToolArgumentError.new("Invalid URL: #{url} - #{e.message}", cause: e)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# File: lib/legate/tools/base_async_job_tool.rb
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative '../tool'
|
|
5
|
+
require_relative '../errors'
|
|
6
|
+
require 'concurrent'
|
|
7
|
+
require 'securerandom'
|
|
8
|
+
require 'json'
|
|
9
|
+
|
|
10
|
+
module Legate
|
|
11
|
+
module Tools
|
|
12
|
+
# Abstract base class for tools that initiate asynchronous background tasks via threads.
|
|
13
|
+
class BaseAsyncJobTool < Legate::Tool
|
|
14
|
+
tool_description "Base class for tools that initiate long-running tasks via background threads. Subclasses must implement `worker_class` and `prepare_job_arguments`. Use 'check_job_status' tool to retrieve results."
|
|
15
|
+
|
|
16
|
+
# --- In-Memory Job Results Storage ---
|
|
17
|
+
class << self
|
|
18
|
+
def job_results
|
|
19
|
+
@job_results ||= Concurrent::Map.new
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Subclasses MUST override this method to return the worker class
|
|
24
|
+
# that should be executed.
|
|
25
|
+
# @return [Class] The worker class (must respond to #perform).
|
|
26
|
+
def worker_class
|
|
27
|
+
raise NotImplementedError, "#{self.class.name} must implement #worker_class"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Subclasses MUST override this method to prepare the arguments
|
|
31
|
+
# for the worker's perform method based on the Legate tool's parameters and context.
|
|
32
|
+
# Note: Arguments must be simple types serializable to JSON (strings, numbers, bools, arrays, hashes).
|
|
33
|
+
# @param params [Hash] The validated parameters passed to the Legate tool.
|
|
34
|
+
# @param context [Legate::ToolContext] Contextual information (session_id, etc.).
|
|
35
|
+
# @return [Array] An array of arguments to be passed to the worker's perform method.
|
|
36
|
+
def prepare_job_arguments(params, context)
|
|
37
|
+
raise NotImplementedError, "#{self.class.name} must implement #prepare_job_arguments(params, context)"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Overrides Legate::Tool#perform_execution to spawn a background thread.
|
|
41
|
+
# @param params [Hash] The validated parameters.
|
|
42
|
+
# @param context [Legate::ToolContext] The execution context.
|
|
43
|
+
# @return [Hash] { status: :pending, job_id: ... } or { status: :error, ... }
|
|
44
|
+
private def perform_execution(params, context)
|
|
45
|
+
jid = SecureRandom.uuid
|
|
46
|
+
worker = worker_class.new
|
|
47
|
+
args = prepare_job_arguments(params, context)
|
|
48
|
+
|
|
49
|
+
BaseAsyncJobTool.job_results[jid] = { 'status' => 'pending' }
|
|
50
|
+
|
|
51
|
+
Legate.logger.info("Spawning background task for worker '#{worker_class.name}' for tool '#{name}'. Job ID: #{jid}")
|
|
52
|
+
Legate.logger.debug("Job Args: #{args.inspect}")
|
|
53
|
+
|
|
54
|
+
Concurrent::Promises.future do
|
|
55
|
+
worker.perform(jid, *args)
|
|
56
|
+
rescue StandardError => e
|
|
57
|
+
BaseAsyncJobTool.job_results[jid] = { 'status' => 'error', 'error_message' => "#{e.class}: #{e.message}" }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
{ status: :pending, job_id: jid, message: "Job #{jid} has been submitted." }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# --- Static Helpers for Workers to Store Status/Results --- #
|
|
64
|
+
|
|
65
|
+
# Helper method for workers to call at the beginning of their perform method
|
|
66
|
+
# to indicate the job has started processing.
|
|
67
|
+
# @param jid [String] The Job ID.
|
|
68
|
+
def self.store_job_pending(jid)
|
|
69
|
+
job_results[jid] = { 'status' => 'pending' }
|
|
70
|
+
Legate.logger.debug("Stored pending status for job #{jid}")
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Helper method for workers to call upon completion to store their results.
|
|
74
|
+
# @param jid [String] The Job ID.
|
|
75
|
+
# @param result [Object] The result data.
|
|
76
|
+
def self.store_job_result(jid, result)
|
|
77
|
+
job_results[jid] = { 'status' => 'completed', 'result' => result.is_a?(String) ? result : result.to_json }
|
|
78
|
+
Legate.logger.debug("Stored successful result for job #{jid}")
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Helper method for workers to call upon failure to store error information.
|
|
82
|
+
# @param jid [String] The Job ID.
|
|
83
|
+
# @param error_message [String] The error message.
|
|
84
|
+
# @param _error_class [String] The class name of the error (kept for API compatibility).
|
|
85
|
+
def self.store_job_error(jid, error_message, _error_class = 'StandardError')
|
|
86
|
+
job_results[jid] = { 'status' => 'error', 'error_message' => error_message }
|
|
87
|
+
Legate.logger.debug("Stored error result for job #{jid}")
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|