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,190 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# File: lib/legate/agents/sequential_agent.rb
|
|
4
|
+
require_relative '../agent'
|
|
5
|
+
|
|
6
|
+
module Legate
|
|
7
|
+
module Agents
|
|
8
|
+
# SequentialAgent executes a series of sub-agents in a predefined order.
|
|
9
|
+
# Each sub-agent is executed one after another, with the same session and input.
|
|
10
|
+
class SequentialAgent < Legate::Agent
|
|
11
|
+
# Override run_task to execute sub-agents in sequence
|
|
12
|
+
# @param session_id [String] The session ID
|
|
13
|
+
# @param user_input [String] User input to process
|
|
14
|
+
# @param session_service [Legate::SessionService::Base] Session service for persistence
|
|
15
|
+
# @return [Legate::Event] The final agent event
|
|
16
|
+
def run_task(session_id:, user_input:, session_service:)
|
|
17
|
+
# Verify we have sequential sub-agents defined
|
|
18
|
+
unless @definition.sequential_sub_agent_names&.any?
|
|
19
|
+
err_msg = "SequentialAgent '#{name}' has no sequential_sub_agent_names defined."
|
|
20
|
+
Legate.logger.error(err_msg)
|
|
21
|
+
return Legate::Event.new(role: :agent, content: {
|
|
22
|
+
status: :error,
|
|
23
|
+
error_message: err_msg,
|
|
24
|
+
error_class: 'ConfigurationError'
|
|
25
|
+
})
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# --- Pre-execution Checks --- #
|
|
29
|
+
unless running?
|
|
30
|
+
err_msg = "Agent '#{name}' is not running. Call agent.start before run_task, " \
|
|
31
|
+
'or use agent.ask (which starts automatically).'
|
|
32
|
+
Legate.logger.error(err_msg)
|
|
33
|
+
return Legate::Event.new(role: :agent, content: { status: :error, error_message: err_msg })
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
session = session_service.get_session(session_id: session_id)
|
|
37
|
+
unless session
|
|
38
|
+
err_msg = "Session not found: #{session_id}"
|
|
39
|
+
Legate.logger.error(err_msg)
|
|
40
|
+
return Legate::Event.new(role: :agent, content: { status: :error, error_message: err_msg })
|
|
41
|
+
end
|
|
42
|
+
# --------------------------- #
|
|
43
|
+
|
|
44
|
+
# Log user input to the SequentialAgent itself
|
|
45
|
+
user_event = Legate::Event.new(role: :user, content: user_input)
|
|
46
|
+
session_service.append_event(session_id: session_id, event: user_event)
|
|
47
|
+
|
|
48
|
+
# Log the execution sequence start
|
|
49
|
+
Legate.logger.info("SequentialAgent '#{name}' starting execution of #{@definition.sequential_sub_agent_names.size} sub-agents in sequence.")
|
|
50
|
+
|
|
51
|
+
# Track results of all sub-agents
|
|
52
|
+
all_results = []
|
|
53
|
+
final_result = nil
|
|
54
|
+
current_input = user_input # Start with the original user input
|
|
55
|
+
|
|
56
|
+
# Execute each sub-agent in order
|
|
57
|
+
@definition.sequential_sub_agent_names.each_with_index do |sub_agent_name, index|
|
|
58
|
+
sub_agent = find_sub_agent(sub_agent_name)
|
|
59
|
+
unless sub_agent
|
|
60
|
+
err_msg = "Sub-agent '#{sub_agent_name}' not found for SequentialAgent '#{name}'."
|
|
61
|
+
Legate.logger.error(err_msg)
|
|
62
|
+
final_result = {
|
|
63
|
+
status: :error,
|
|
64
|
+
error_message: err_msg,
|
|
65
|
+
error_class: 'MissingSubAgentError',
|
|
66
|
+
step: index + 1,
|
|
67
|
+
total_steps: @definition.sequential_sub_agent_names.size,
|
|
68
|
+
previous_results: all_results.map.with_index { |r, i| { agent: @definition.sequential_sub_agent_names.to_a[i], result: r } }
|
|
69
|
+
}
|
|
70
|
+
break # Stop the sequence on error
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Start the sub-agent if it's not already running
|
|
74
|
+
sub_agent.start unless sub_agent.running?
|
|
75
|
+
|
|
76
|
+
# If this is not the first agent, try to augment its input with previous results
|
|
77
|
+
if index > 0 && all_results.any?
|
|
78
|
+
# Get previous agent name - handle both Array and Set types
|
|
79
|
+
previous_agent_name = nil
|
|
80
|
+
previous_agent_name = if @definition.sequential_sub_agent_names.is_a?(Set)
|
|
81
|
+
# For Set, convert to Array and get the element
|
|
82
|
+
@definition.sequential_sub_agent_names.to_a[index - 1]
|
|
83
|
+
else
|
|
84
|
+
# For Array, access directly
|
|
85
|
+
@definition.sequential_sub_agent_names[index - 1]
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
previous_output_key = find_sub_agent(previous_agent_name)&.definition&.output_key
|
|
89
|
+
|
|
90
|
+
if previous_output_key && session_service.respond_to?(:get_state)
|
|
91
|
+
# Get the previous agent's result from session state
|
|
92
|
+
previous_result = session_service.get_state(session_id: session_id, key: previous_output_key)
|
|
93
|
+
|
|
94
|
+
if previous_result && previous_result.is_a?(Hash) && previous_result[:result]
|
|
95
|
+
# Enhanced input with previous result
|
|
96
|
+
current_input = "#{current_input}\n\nHere is the result from the previous step (#{previous_agent_name}):\n#{previous_result[:result]}"
|
|
97
|
+
Legate.logger.info("Enhanced input for sub-agent '#{sub_agent_name}' with previous result from '#{previous_agent_name}'")
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Execute the sub-agent with the updated input
|
|
103
|
+
begin
|
|
104
|
+
Legate.logger.info("SequentialAgent '#{name}' executing sub-agent '#{sub_agent_name}' (step #{index + 1}/#{@definition.sequential_sub_agent_names.size}).")
|
|
105
|
+
sub_result = sub_agent.run_task(
|
|
106
|
+
session_id: session_id,
|
|
107
|
+
user_input: current_input,
|
|
108
|
+
session_service: session_service
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
# Record the result
|
|
112
|
+
all_results << sub_result.content
|
|
113
|
+
|
|
114
|
+
# Check for error to break sequence
|
|
115
|
+
if sub_result.content[:status] == :error
|
|
116
|
+
Legate.logger.warn("Sub-agent '#{sub_agent_name}' returned error, breaking sequence: #{sub_result.content[:error_message]}")
|
|
117
|
+
final_result = {
|
|
118
|
+
status: :error,
|
|
119
|
+
error_message: "Error in sub-agent '#{sub_agent_name}': #{sub_result.content[:error_message]}",
|
|
120
|
+
error_class: sub_result.content[:error_class] || 'SubAgentError',
|
|
121
|
+
step: index + 1,
|
|
122
|
+
total_steps: @definition.sequential_sub_agent_names.size,
|
|
123
|
+
sub_agent: sub_agent_name.to_s,
|
|
124
|
+
sub_result: sub_result.content,
|
|
125
|
+
previous_results: all_results.map.with_index { |r, i| { agent: @definition.sequential_sub_agent_names.to_a[i], result: r } }
|
|
126
|
+
}
|
|
127
|
+
break # Stop the sequence on error
|
|
128
|
+
elsif sub_result.content[:result]
|
|
129
|
+
# If the sub-agent succeeded, update the current_input to include its result for the next agent
|
|
130
|
+
# Store the raw result for potential state-based chaining in next iteration
|
|
131
|
+
current_input = sub_result.content[:result].to_s
|
|
132
|
+
end
|
|
133
|
+
rescue StandardError => e
|
|
134
|
+
Legate.logger.error("Error executing sub-agent '#{sub_agent_name}': #{e.class} - #{e.message}\n#{e.backtrace.join("\n")}")
|
|
135
|
+
final_result = {
|
|
136
|
+
status: :error,
|
|
137
|
+
error_message: "Exception in sub-agent '#{sub_agent_name}': #{e.message}",
|
|
138
|
+
error_class: e.class.name,
|
|
139
|
+
step: index + 1,
|
|
140
|
+
total_steps: @definition.sequential_sub_agent_names.size,
|
|
141
|
+
sub_agent: sub_agent_name.to_s,
|
|
142
|
+
previous_results: all_results.map.with_index { |r, i| { agent: @definition.sequential_sub_agent_names.to_a[i], result: r } }
|
|
143
|
+
}
|
|
144
|
+
break # Stop the sequence on error
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# If we didn't set a final_result due to an error, create a success result with all sub-results
|
|
149
|
+
if final_result.nil?
|
|
150
|
+
final_result = {
|
|
151
|
+
status: :success,
|
|
152
|
+
result: "Completed sequential execution of #{@definition.sequential_sub_agent_names.size} sub-agents",
|
|
153
|
+
steps_completed: @definition.sequential_sub_agent_names.size,
|
|
154
|
+
sub_results: all_results.map.with_index { |r, i| { agent: @definition.sequential_sub_agent_names.to_a[i], result: r } }
|
|
155
|
+
}
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Create the final event
|
|
159
|
+
final_agent_event = Legate::Event.new(role: :agent, content: final_result)
|
|
160
|
+
|
|
161
|
+
# Log the final event to the session
|
|
162
|
+
session_service.append_event(session_id: session_id, event: final_agent_event)
|
|
163
|
+
|
|
164
|
+
# --- MAS: Store result in session state if output_key is defined --- #
|
|
165
|
+
if @definition.respond_to?(:output_key) && @definition.output_key && final_agent_event
|
|
166
|
+
output_value = final_agent_event.content # Store the entire content hash
|
|
167
|
+
|
|
168
|
+
# Make sure the output value is serializable
|
|
169
|
+
serialized_value = begin
|
|
170
|
+
# Convert to JSON and back to ensure it's serializable
|
|
171
|
+
JSON.parse(output_value.to_json)
|
|
172
|
+
rescue StandardError => e
|
|
173
|
+
Legate.logger.warn("SequentialAgent '#{@name}': Failed to serialize output value: #{e.message}. Using simplified value.")
|
|
174
|
+
{ status: output_value[:status], result: output_value[:result].to_s }
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
Legate.logger.info("SequentialAgent '#{@name}' storing output to session state with key '#{@definition.output_key}' for session '#{session_id}'.")
|
|
178
|
+
if session_service.respond_to?(:set_state)
|
|
179
|
+
session_service.set_state(session_id: session_id, key: @definition.output_key, value: serialized_value)
|
|
180
|
+
else
|
|
181
|
+
Legate.logger.warn("SequentialAgent '#{@name}': Session service does not support :set_state. Cannot store output for key '#{@definition.output_key}'.")
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
# --- End MAS State Management --- #
|
|
185
|
+
|
|
186
|
+
final_agent_event
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# File: lib/legate/agents.rb
|
|
4
|
+
# This manifest file loads all agent implementations for workflow agents
|
|
5
|
+
|
|
6
|
+
require_relative 'agents/sequential_agent'
|
|
7
|
+
require_relative 'agents/parallel_agent'
|
|
8
|
+
require_relative 'agents/loop_agent'
|
|
9
|
+
|
|
10
|
+
module Legate
|
|
11
|
+
module Agents
|
|
12
|
+
# This module contains specialized agent implementations for workflow composition
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# File: lib/legate/auth/config.rb
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'securerandom'
|
|
5
|
+
|
|
6
|
+
module Legate
|
|
7
|
+
module Auth
|
|
8
|
+
# Configuration container used during the authentication flow.
|
|
9
|
+
# Holds the authentication scheme, credential, and request/response details
|
|
10
|
+
# needed for interactive authentication flows.
|
|
11
|
+
class Config
|
|
12
|
+
# @return [Legate::Auth::Scheme] The authentication scheme
|
|
13
|
+
attr_reader :scheme
|
|
14
|
+
|
|
15
|
+
# @return [Legate::Auth::Credential] The credential information
|
|
16
|
+
attr_reader :credential
|
|
17
|
+
|
|
18
|
+
# @return [String, nil] The unique ID for this authentication request
|
|
19
|
+
attr_reader :auth_request_id
|
|
20
|
+
|
|
21
|
+
# @return [String, nil] The authorization URI for interactive flows
|
|
22
|
+
attr_accessor :auth_uri
|
|
23
|
+
|
|
24
|
+
# @return [String, nil] The redirect URI for OAuth2/OIDC flows
|
|
25
|
+
attr_accessor :redirect_uri
|
|
26
|
+
|
|
27
|
+
# @return [String, nil] The state parameter for CSRF protection
|
|
28
|
+
attr_accessor :state
|
|
29
|
+
|
|
30
|
+
# @return [Hash, nil] The PKCE parameters (code_verifier, etc.)
|
|
31
|
+
attr_accessor :pkce
|
|
32
|
+
|
|
33
|
+
# @return [String, nil] The authorization response URI from the provider
|
|
34
|
+
attr_accessor :response_uri
|
|
35
|
+
|
|
36
|
+
# For backwards compatibility
|
|
37
|
+
alias auth_response_uri response_uri
|
|
38
|
+
alias auth_response_uri= response_uri=
|
|
39
|
+
|
|
40
|
+
# @return [Hash, nil] Additional options for the authentication process
|
|
41
|
+
attr_accessor :options
|
|
42
|
+
|
|
43
|
+
# Initialize a new authentication configuration
|
|
44
|
+
# @param scheme [Legate::Auth::Scheme] The authentication scheme
|
|
45
|
+
# @param credential [Legate::Auth::Credential] The credential information
|
|
46
|
+
# @param auth_request_id [String, nil] The unique ID for this authentication request
|
|
47
|
+
# @param options [Hash, nil] Additional options for the authentication process
|
|
48
|
+
def initialize(scheme:, credential:, auth_request_id: nil, options: {})
|
|
49
|
+
@scheme = scheme
|
|
50
|
+
@credential = credential
|
|
51
|
+
@auth_request_id = auth_request_id || Legate::Auth.generate_request_id
|
|
52
|
+
@options = options || {}
|
|
53
|
+
@auth_uri = nil
|
|
54
|
+
@redirect_uri = nil
|
|
55
|
+
@state = nil
|
|
56
|
+
@pkce = nil
|
|
57
|
+
@response_uri = nil
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Build the authorization URI for interactive flows
|
|
61
|
+
# @param redirect_uri [String, nil] The redirect URI for the authorization request
|
|
62
|
+
# @param state [String, nil] A state parameter for CSRF protection
|
|
63
|
+
# @return [String, Hash] The authorization URI or a hash with URI and additional parameters
|
|
64
|
+
def build_authorization_uri(redirect_uri = nil, state = nil)
|
|
65
|
+
@redirect_uri = redirect_uri
|
|
66
|
+
@state = state || @options[:state] || SecureRandom.hex(16)
|
|
67
|
+
|
|
68
|
+
# For OAuth2 schemes with detailed return values including PKCE
|
|
69
|
+
result = @scheme.build_authorization_uri(self, @redirect_uri, @state)
|
|
70
|
+
|
|
71
|
+
if result.is_a?(Hash) && result[:uri]
|
|
72
|
+
@auth_uri = result[:uri]
|
|
73
|
+
@state = result[:state] if result[:state]
|
|
74
|
+
@pkce = result[:pkce] if result[:pkce]
|
|
75
|
+
@auth_uri
|
|
76
|
+
else
|
|
77
|
+
# For backwards compatibility with simpler schemes
|
|
78
|
+
@auth_uri = result
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Convert to a hash for serialization
|
|
83
|
+
# @param include_credentials [Boolean] Whether to include credential details (use carefully)
|
|
84
|
+
# @return [Hash] A hash representation of the config
|
|
85
|
+
def to_h(include_credentials: false)
|
|
86
|
+
{
|
|
87
|
+
auth_request_id: @auth_request_id,
|
|
88
|
+
scheme_type: @scheme.scheme_type,
|
|
89
|
+
auth_uri: @auth_uri,
|
|
90
|
+
redirect_uri: @redirect_uri,
|
|
91
|
+
state: @state,
|
|
92
|
+
pkce: @pkce,
|
|
93
|
+
response_uri: @response_uri,
|
|
94
|
+
options: @options
|
|
95
|
+
}.tap do |h|
|
|
96
|
+
h[:credential] = @credential.to_h if include_credentials
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Creates a Config from a hash representation
|
|
101
|
+
# @param hash [Hash] The hash representation
|
|
102
|
+
# @param scheme [Legate::Auth::Scheme] The authentication scheme (required if not recreating from complete data)
|
|
103
|
+
# @param credential [Legate::Auth::Credential] The credential information (required if not recreating from complete data)
|
|
104
|
+
# @return [Legate::Auth::Config] A new Config instance
|
|
105
|
+
# @raise [Legate::Auth::ConfigurationError] If required parameters are missing
|
|
106
|
+
def self.from_h(hash, scheme: nil, credential: nil)
|
|
107
|
+
scheme ||= hash[:scheme]
|
|
108
|
+
credential ||= hash[:credential]
|
|
109
|
+
|
|
110
|
+
raise Legate::Auth::ConfigurationError, 'Scheme and credential must be provided' unless scheme && credential
|
|
111
|
+
|
|
112
|
+
config = new(
|
|
113
|
+
scheme: scheme,
|
|
114
|
+
credential: credential,
|
|
115
|
+
auth_request_id: hash[:auth_request_id],
|
|
116
|
+
options: hash[:options] || {}
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
config.auth_uri = hash[:auth_uri]
|
|
120
|
+
config.redirect_uri = hash[:redirect_uri]
|
|
121
|
+
config.state = hash[:state]
|
|
122
|
+
config.pkce = hash[:pkce]
|
|
123
|
+
|
|
124
|
+
# Handle both new and old response URI keys
|
|
125
|
+
config.response_uri = hash[:response_uri] || hash[:auth_response_uri]
|
|
126
|
+
|
|
127
|
+
config
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Validates a response against this configuration
|
|
131
|
+
# @param response_config [Legate::Auth::Config] The response configuration
|
|
132
|
+
# @return [Boolean] True if the response is valid for this request
|
|
133
|
+
# @raise [Legate::Auth::ConfigurationError] If the response is invalid
|
|
134
|
+
def validate_response!(response_config)
|
|
135
|
+
# Check request ID
|
|
136
|
+
raise Legate::Auth::ConfigurationError, 'Authentication response ID does not match request ID' unless response_config.auth_request_id == @auth_request_id
|
|
137
|
+
|
|
138
|
+
# Check that we have an auth response URI
|
|
139
|
+
raise Legate::Auth::ConfigurationError, 'Authentication response does not contain a response URI' unless response_config.response_uri
|
|
140
|
+
|
|
141
|
+
# Check state if we had one
|
|
142
|
+
raise Legate::Auth::ConfigurationError, 'Authentication response state does not match request state' if @state && response_config.state && response_config.state != @state
|
|
143
|
+
|
|
144
|
+
true
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'error'
|
|
4
|
+
require_relative 'config'
|
|
5
|
+
require_relative 'credential'
|
|
6
|
+
require_relative 'exchanged_credential'
|
|
7
|
+
require_relative 'token_store'
|
|
8
|
+
|
|
9
|
+
module Legate
|
|
10
|
+
module Auth
|
|
11
|
+
# Base class for authentication coordinators that handle the fiber-based authentication flow.
|
|
12
|
+
# Coordinators are responsible for managing the state of an authentication flow,
|
|
13
|
+
# including pausing execution, waiting for user input/authentication, and resuming
|
|
14
|
+
# with appropriate credentials.
|
|
15
|
+
class Coordinator
|
|
16
|
+
# Default timeout for authentication flow (in seconds)
|
|
17
|
+
DEFAULT_TIMEOUT = 300
|
|
18
|
+
|
|
19
|
+
# Authentication status codes
|
|
20
|
+
module Status
|
|
21
|
+
PENDING = :pending
|
|
22
|
+
COMPLETED = :completed
|
|
23
|
+
FAILED = :failed
|
|
24
|
+
TIMEOUT = :timeout
|
|
25
|
+
CANCELLED = :cancelled
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Initialize a new authentication coordinator
|
|
29
|
+
# @param scheme [Legate::Auth::Scheme] The authentication scheme to use
|
|
30
|
+
# @param credential [Legate::Auth::Credential] The credential to use
|
|
31
|
+
# @param session_service [Legate::SessionService::Base] The session service for state persistence
|
|
32
|
+
# @param token_store [Legate::Auth::TokenStore, nil] Optional token store for caching tokens
|
|
33
|
+
# @param timeout [Integer, nil] Optional timeout in seconds (nil for no timeout)
|
|
34
|
+
def initialize(scheme:, credential:, session_service:, token_store: nil, timeout: DEFAULT_TIMEOUT)
|
|
35
|
+
@scheme = scheme
|
|
36
|
+
@credential = credential
|
|
37
|
+
@session_service = session_service
|
|
38
|
+
@token_store = token_store || TokenStore.new(session_service)
|
|
39
|
+
@timeout = timeout
|
|
40
|
+
@request_id = SecureRandom.uuid
|
|
41
|
+
@status = Status::PENDING
|
|
42
|
+
@start_time = nil
|
|
43
|
+
@auth_fiber = nil
|
|
44
|
+
@result = nil
|
|
45
|
+
@error = nil
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Start the authentication flow
|
|
49
|
+
# @return [Hash] The authentication request details to be sent to the client
|
|
50
|
+
def start
|
|
51
|
+
# Create a new fiber for this authentication flow
|
|
52
|
+
@auth_fiber = Fiber.new do
|
|
53
|
+
@start_time = Time.now
|
|
54
|
+
@result = authenticate
|
|
55
|
+
@status = Status::COMPLETED
|
|
56
|
+
rescue Legate::Auth::Error => e
|
|
57
|
+
@error = e
|
|
58
|
+
@status = Status::FAILED
|
|
59
|
+
nil
|
|
60
|
+
rescue StandardError => e
|
|
61
|
+
@error = Legate::Auth::Error.new("Unexpected error during authentication: #{e.message}", e)
|
|
62
|
+
@status = Status::FAILED
|
|
63
|
+
nil
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Start the fiber to get the initial authentication request
|
|
67
|
+
auth_request = @auth_fiber.resume
|
|
68
|
+
|
|
69
|
+
# Record start of authentication in session
|
|
70
|
+
save_auth_state
|
|
71
|
+
|
|
72
|
+
# Return the authentication request to be sent to the client
|
|
73
|
+
{
|
|
74
|
+
request_id: @request_id,
|
|
75
|
+
scheme_type: @scheme.scheme_type,
|
|
76
|
+
auth_request: auth_request
|
|
77
|
+
}
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Resume the authentication flow with a response from the client
|
|
81
|
+
# @param response [Hash] The response from the client
|
|
82
|
+
# @return [Legate::Auth::ExchangedCredential, nil] The resulting credential or nil on failure
|
|
83
|
+
def resume(response)
|
|
84
|
+
raise Legate::Auth::Error, "Authentication flow is not in progress (status: #{@status})" unless @status == Status::PENDING
|
|
85
|
+
|
|
86
|
+
# Check for timeout
|
|
87
|
+
if @timeout && Time.now - @start_time > @timeout
|
|
88
|
+
@status = Status::TIMEOUT
|
|
89
|
+
@error = Legate::Auth::Error.new("Authentication timed out after #{@timeout} seconds")
|
|
90
|
+
save_auth_state
|
|
91
|
+
return nil
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Resume the fiber with the client response
|
|
95
|
+
begin
|
|
96
|
+
result = @auth_fiber.resume(response)
|
|
97
|
+
|
|
98
|
+
# If the fiber yields again, we need more input from the client
|
|
99
|
+
if @auth_fiber.alive?
|
|
100
|
+
# Return the next authentication request
|
|
101
|
+
return {
|
|
102
|
+
request_id: @request_id,
|
|
103
|
+
scheme_type: @scheme.scheme_type,
|
|
104
|
+
auth_request: result
|
|
105
|
+
}
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Authentication completed
|
|
109
|
+
@status = Status::COMPLETED if @status == Status::PENDING
|
|
110
|
+
save_auth_state
|
|
111
|
+
|
|
112
|
+
# Store the token if we have one
|
|
113
|
+
if @result && @result.is_a?(Legate::Auth::ExchangedCredential) && @token_store
|
|
114
|
+
cache_key = generate_cache_key(@scheme, @credential)
|
|
115
|
+
@token_store.store(cache_key, @result)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
@result
|
|
119
|
+
rescue StandardError => e
|
|
120
|
+
@error = Legate::Auth::Error.new("Error resuming authentication: #{e.message}", e)
|
|
121
|
+
@status = Status::FAILED
|
|
122
|
+
save_auth_state
|
|
123
|
+
nil
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Cancel the authentication flow
|
|
128
|
+
# @param reason [String, nil] Optional reason for cancellation
|
|
129
|
+
# @return [Boolean] True if the flow was successfully cancelled
|
|
130
|
+
def cancel(reason = nil)
|
|
131
|
+
return false unless @status == Status::PENDING
|
|
132
|
+
|
|
133
|
+
@status = Status::CANCELLED
|
|
134
|
+
@error = Legate::Auth::Error.new("Authentication cancelled#{reason ? ": #{reason}" : ''}")
|
|
135
|
+
save_auth_state
|
|
136
|
+
true
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Get the current status of the authentication flow
|
|
140
|
+
# @return [Symbol] The current status
|
|
141
|
+
attr_reader :status
|
|
142
|
+
|
|
143
|
+
# Get the error that occurred during authentication, if any
|
|
144
|
+
# @return [Legate::Auth::Error, nil] The error or nil if no error occurred
|
|
145
|
+
attr_reader :error
|
|
146
|
+
|
|
147
|
+
# Get the result of the authentication flow
|
|
148
|
+
# @return [Legate::Auth::ExchangedCredential, nil] The resulting credential or nil if not completed
|
|
149
|
+
attr_reader :result
|
|
150
|
+
|
|
151
|
+
# Check if the authentication flow is complete
|
|
152
|
+
# @return [Boolean] True if the flow is complete (success or failure)
|
|
153
|
+
def complete?
|
|
154
|
+
@status != Status::PENDING
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Check if the authentication flow is successful
|
|
158
|
+
# @return [Boolean] True if the flow completed successfully
|
|
159
|
+
def success?
|
|
160
|
+
@status == Status::COMPLETED && @result
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
protected
|
|
164
|
+
|
|
165
|
+
# Main authentication method to be implemented by subclasses
|
|
166
|
+
# This method should use Fiber.yield to pause execution and wait for client input
|
|
167
|
+
# @return [Legate::Auth::ExchangedCredential] The authenticated credential
|
|
168
|
+
# @raise [Legate::Auth::Error] If authentication fails
|
|
169
|
+
def authenticate
|
|
170
|
+
raise NotImplementedError, "Subclasses must implement the 'authenticate' method"
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Generate a cache key for the authentication token
|
|
174
|
+
# @param scheme [Legate::Auth::Scheme] The authentication scheme
|
|
175
|
+
# @param credential [Legate::Auth::Credential] The credential
|
|
176
|
+
# @return [String] The cache key
|
|
177
|
+
def generate_cache_key(scheme, credential)
|
|
178
|
+
require 'digest/sha2'
|
|
179
|
+
|
|
180
|
+
# Create a unique key based on scheme and credential
|
|
181
|
+
parts = [
|
|
182
|
+
scheme.scheme_type.to_s,
|
|
183
|
+
credential.auth_type.to_s
|
|
184
|
+
]
|
|
185
|
+
|
|
186
|
+
# Add scheme-specific information
|
|
187
|
+
case scheme.scheme_type
|
|
188
|
+
when :api_key
|
|
189
|
+
parts << credential[:api_key, resolve_env: false].to_s
|
|
190
|
+
when :http_bearer
|
|
191
|
+
parts << credential[:bearer_token, resolve_env: false].to_s
|
|
192
|
+
when :oauth2, :oidc
|
|
193
|
+
parts << credential[:client_id, resolve_env: false].to_s
|
|
194
|
+
parts << (credential[:scope, resolve_env: false] || '').to_s
|
|
195
|
+
when :service_account
|
|
196
|
+
parts << credential[:client_email, resolve_env: false].to_s
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
"auth_#{Digest::SHA256.hexdigest(parts.join(':'))}"
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
private
|
|
203
|
+
|
|
204
|
+
# Save the current authentication state to the session
|
|
205
|
+
def save_auth_state
|
|
206
|
+
state = {
|
|
207
|
+
request_id: @request_id,
|
|
208
|
+
scheme_type: @scheme.scheme_type,
|
|
209
|
+
status: @status,
|
|
210
|
+
start_time: @start_time&.iso8601,
|
|
211
|
+
error: @error&.message
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
@session_service.save_scoped_state('auth_flow', @request_id, state)
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../coordinator'
|
|
4
|
+
require_relative '../schemes/oauth2'
|
|
5
|
+
require_relative '../config'
|
|
6
|
+
|
|
7
|
+
module Legate
|
|
8
|
+
module Auth
|
|
9
|
+
module Coordinators
|
|
10
|
+
# OAuth2Coordinator handles the interactive OAuth2 authentication flow using fibers.
|
|
11
|
+
# It manages pausing execution to request user authorization, and resuming once
|
|
12
|
+
# the authorization code is received.
|
|
13
|
+
class OAuth2Coordinator < Coordinator
|
|
14
|
+
# Authentication steps for OAuth2
|
|
15
|
+
module Steps
|
|
16
|
+
AUTHORIZATION = :authorization
|
|
17
|
+
TOKEN_EXCHANGE = :token_exchange
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Initialize a new OAuth2 coordinator
|
|
21
|
+
# @param scheme [Legate::Auth::Schemes::OAuth2] The OAuth2 scheme
|
|
22
|
+
# @param credential [Legate::Auth::Credential] The credential with client information
|
|
23
|
+
# @param session_service [Legate::SessionService::Base] The session service
|
|
24
|
+
# @param token_store [Legate::Auth::TokenStore, nil] Optional token store
|
|
25
|
+
# @param timeout [Integer, nil] Optional timeout in seconds
|
|
26
|
+
# @param redirect_uri [String, nil] Optional redirect URI
|
|
27
|
+
def initialize(scheme:, credential:, session_service:, token_store: nil, timeout: DEFAULT_TIMEOUT, redirect_uri: nil)
|
|
28
|
+
super(scheme: scheme, credential: credential, session_service: session_service, token_store: token_store, timeout: timeout)
|
|
29
|
+
|
|
30
|
+
raise ArgumentError, "Expected an OAuth2 scheme, got #{scheme.class}" unless scheme.is_a?(Legate::Auth::Schemes::OAuth2)
|
|
31
|
+
|
|
32
|
+
raise ArgumentError, "Credential must have auth_type :oauth2, got #{credential.auth_type}" unless credential.auth_type == :oauth2
|
|
33
|
+
|
|
34
|
+
@redirect_uri = redirect_uri
|
|
35
|
+
@current_step = Steps::AUTHORIZATION
|
|
36
|
+
@auth_config = nil
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
protected
|
|
40
|
+
|
|
41
|
+
# Implement the OAuth2 authentication flow
|
|
42
|
+
# @return [Legate::Auth::ExchangedCredential] The authenticated credential
|
|
43
|
+
# @raise [Legate::Auth::Error] If authentication fails
|
|
44
|
+
def authenticate
|
|
45
|
+
# Step 1: Create authorization request
|
|
46
|
+
@current_step = Steps::AUTHORIZATION
|
|
47
|
+
authorization_response = request_authorization
|
|
48
|
+
|
|
49
|
+
# Step 2: Exchange code for tokens
|
|
50
|
+
@current_step = Steps::TOKEN_EXCHANGE
|
|
51
|
+
exchange_code_for_token(authorization_response)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
# Request authorization from the user
|
|
57
|
+
# @return [Hash] The authorization response from the client
|
|
58
|
+
def request_authorization
|
|
59
|
+
# Create a config for the authorization request
|
|
60
|
+
@auth_config = Legate::Auth::Config.new(
|
|
61
|
+
scheme: @scheme,
|
|
62
|
+
credential: @credential,
|
|
63
|
+
options: { request_id: @request_id }
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
# Build the authorization URI
|
|
67
|
+
authorization_uri = @auth_config.build_authorization_uri(@redirect_uri)
|
|
68
|
+
|
|
69
|
+
# Yield to pause execution and wait for authorization response
|
|
70
|
+
response = Fiber.yield({
|
|
71
|
+
type: 'authorization_request',
|
|
72
|
+
authorization_url: authorization_uri[:uri],
|
|
73
|
+
state: authorization_uri[:state],
|
|
74
|
+
redirect_uri: @redirect_uri
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
# Validate the response
|
|
78
|
+
raise Legate::Auth::Error, "Invalid authorization response: expected Hash, got #{response.class}" unless response.is_a?(Hash)
|
|
79
|
+
|
|
80
|
+
raise Legate::Auth::Error, 'Missing response_uri in authorization response' unless response['response_uri']
|
|
81
|
+
|
|
82
|
+
response
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Exchange the authorization code for tokens
|
|
86
|
+
# @param authorization_response [Hash] The authorization response from the client
|
|
87
|
+
# @return [Legate::Auth::ExchangedCredential] The authenticated credential
|
|
88
|
+
# @raise [Legate::Auth::TokenExchangeError] If token exchange fails
|
|
89
|
+
def exchange_code_for_token(authorization_response)
|
|
90
|
+
# Update the config with the response URI
|
|
91
|
+
@auth_config.response_uri = authorization_response['response_uri']
|
|
92
|
+
|
|
93
|
+
# Exchange the code for tokens
|
|
94
|
+
@scheme.exchange_token(@auth_config, @credential)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|