a2a-ruby 1.0.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/.rspec +3 -0
- data/.rubocop.yml +137 -0
- data/.simplecov +46 -0
- data/.yardopts +10 -0
- data/CHANGELOG.md +33 -0
- data/CODE_OF_CONDUCT.md +128 -0
- data/CONTRIBUTING.md +165 -0
- data/Gemfile +43 -0
- data/Guardfile +34 -0
- data/LICENSE.txt +21 -0
- data/PUBLISHING_CHECKLIST.md +214 -0
- data/README.md +171 -0
- data/Rakefile +165 -0
- data/docs/agent_execution.md +309 -0
- data/docs/api_reference.md +792 -0
- data/docs/configuration.md +780 -0
- data/docs/events.md +475 -0
- data/docs/getting_started.md +668 -0
- data/docs/integration.md +262 -0
- data/docs/server_apps.md +621 -0
- data/docs/troubleshooting.md +765 -0
- data/lib/a2a/client/api_methods.rb +263 -0
- data/lib/a2a/client/auth/api_key.rb +161 -0
- data/lib/a2a/client/auth/interceptor.rb +288 -0
- data/lib/a2a/client/auth/jwt.rb +189 -0
- data/lib/a2a/client/auth/oauth2.rb +146 -0
- data/lib/a2a/client/auth.rb +137 -0
- data/lib/a2a/client/base.rb +316 -0
- data/lib/a2a/client/config.rb +210 -0
- data/lib/a2a/client/connection_pool.rb +233 -0
- data/lib/a2a/client/http_client.rb +524 -0
- data/lib/a2a/client/json_rpc_handler.rb +136 -0
- data/lib/a2a/client/middleware/circuit_breaker_interceptor.rb +245 -0
- data/lib/a2a/client/middleware/logging_interceptor.rb +371 -0
- data/lib/a2a/client/middleware/rate_limit_interceptor.rb +142 -0
- data/lib/a2a/client/middleware/retry_interceptor.rb +161 -0
- data/lib/a2a/client/middleware.rb +116 -0
- data/lib/a2a/client/performance_tracker.rb +60 -0
- data/lib/a2a/configuration/defaults.rb +34 -0
- data/lib/a2a/configuration/environment_loader.rb +76 -0
- data/lib/a2a/configuration/file_loader.rb +115 -0
- data/lib/a2a/configuration/inheritance.rb +101 -0
- data/lib/a2a/configuration/validator.rb +180 -0
- data/lib/a2a/configuration.rb +201 -0
- data/lib/a2a/errors.rb +291 -0
- data/lib/a2a/modules.rb +50 -0
- data/lib/a2a/monitoring/alerting.rb +490 -0
- data/lib/a2a/monitoring/distributed_tracing.rb +398 -0
- data/lib/a2a/monitoring/health_endpoints.rb +204 -0
- data/lib/a2a/monitoring/metrics_collector.rb +438 -0
- data/lib/a2a/monitoring.rb +463 -0
- data/lib/a2a/plugin.rb +358 -0
- data/lib/a2a/plugin_manager.rb +159 -0
- data/lib/a2a/plugins/example_auth.rb +81 -0
- data/lib/a2a/plugins/example_middleware.rb +118 -0
- data/lib/a2a/plugins/example_transport.rb +76 -0
- data/lib/a2a/protocol/agent_card.rb +8 -0
- data/lib/a2a/protocol/agent_card_server.rb +584 -0
- data/lib/a2a/protocol/capability.rb +496 -0
- data/lib/a2a/protocol/json_rpc.rb +254 -0
- data/lib/a2a/protocol/message.rb +8 -0
- data/lib/a2a/protocol/task.rb +8 -0
- data/lib/a2a/rails/a2a_controller.rb +258 -0
- data/lib/a2a/rails/controller_helpers.rb +499 -0
- data/lib/a2a/rails/engine.rb +167 -0
- data/lib/a2a/rails/generators/agent_generator.rb +311 -0
- data/lib/a2a/rails/generators/install_generator.rb +209 -0
- data/lib/a2a/rails/generators/migration_generator.rb +232 -0
- data/lib/a2a/rails/generators/templates/add_a2a_indexes.rb +57 -0
- data/lib/a2a/rails/generators/templates/agent_controller.rb +122 -0
- data/lib/a2a/rails/generators/templates/agent_controller_spec.rb +160 -0
- data/lib/a2a/rails/generators/templates/agent_readme.md +200 -0
- data/lib/a2a/rails/generators/templates/create_a2a_push_notification_configs.rb +68 -0
- data/lib/a2a/rails/generators/templates/create_a2a_tasks.rb +83 -0
- data/lib/a2a/rails/generators/templates/example_agent_controller.rb +228 -0
- data/lib/a2a/rails/generators/templates/initializer.rb +108 -0
- data/lib/a2a/rails/generators/templates/push_notification_config_model.rb +228 -0
- data/lib/a2a/rails/generators/templates/task_model.rb +200 -0
- data/lib/a2a/rails/tasks/a2a.rake +228 -0
- data/lib/a2a/server/a2a_methods.rb +520 -0
- data/lib/a2a/server/agent.rb +537 -0
- data/lib/a2a/server/agent_execution/agent_executor.rb +279 -0
- data/lib/a2a/server/agent_execution/request_context.rb +219 -0
- data/lib/a2a/server/apps/rack_app.rb +311 -0
- data/lib/a2a/server/apps/sinatra_app.rb +261 -0
- data/lib/a2a/server/default_request_handler.rb +350 -0
- data/lib/a2a/server/events/event_consumer.rb +116 -0
- data/lib/a2a/server/events/event_queue.rb +226 -0
- data/lib/a2a/server/example_agent.rb +248 -0
- data/lib/a2a/server/handler.rb +281 -0
- data/lib/a2a/server/middleware/authentication_middleware.rb +212 -0
- data/lib/a2a/server/middleware/cors_middleware.rb +171 -0
- data/lib/a2a/server/middleware/logging_middleware.rb +362 -0
- data/lib/a2a/server/middleware/rate_limit_middleware.rb +382 -0
- data/lib/a2a/server/middleware.rb +213 -0
- data/lib/a2a/server/push_notification_manager.rb +327 -0
- data/lib/a2a/server/request_handler.rb +136 -0
- data/lib/a2a/server/storage/base.rb +141 -0
- data/lib/a2a/server/storage/database.rb +266 -0
- data/lib/a2a/server/storage/memory.rb +274 -0
- data/lib/a2a/server/storage/redis.rb +320 -0
- data/lib/a2a/server/storage.rb +38 -0
- data/lib/a2a/server/task_manager.rb +534 -0
- data/lib/a2a/transport/grpc.rb +481 -0
- data/lib/a2a/transport/http.rb +415 -0
- data/lib/a2a/transport/sse.rb +499 -0
- data/lib/a2a/types/agent_card.rb +540 -0
- data/lib/a2a/types/artifact.rb +99 -0
- data/lib/a2a/types/base_model.rb +223 -0
- data/lib/a2a/types/events.rb +117 -0
- data/lib/a2a/types/message.rb +106 -0
- data/lib/a2a/types/part.rb +288 -0
- data/lib/a2a/types/push_notification.rb +139 -0
- data/lib/a2a/types/security.rb +167 -0
- data/lib/a2a/types/task.rb +154 -0
- data/lib/a2a/types.rb +88 -0
- data/lib/a2a/utils/helpers.rb +245 -0
- data/lib/a2a/utils/message_buffer.rb +278 -0
- data/lib/a2a/utils/performance.rb +247 -0
- data/lib/a2a/utils/rails_detection.rb +97 -0
- data/lib/a2a/utils/structured_logger.rb +306 -0
- data/lib/a2a/utils/time_helpers.rb +167 -0
- data/lib/a2a/utils/validation.rb +8 -0
- data/lib/a2a/version.rb +6 -0
- data/lib/a2a-rails.rb +58 -0
- data/lib/a2a.rb +198 -0
- metadata +437 -0
@@ -0,0 +1,499 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../utils/rails_detection"
|
4
|
+
|
5
|
+
module A2A
|
6
|
+
module Rails
|
7
|
+
##
|
8
|
+
# Controller helpers for A2A Rails integration
|
9
|
+
#
|
10
|
+
# This module provides helper methods for Rails controllers to handle A2A requests,
|
11
|
+
# generate agent cards, and integrate with Rails authentication systems.
|
12
|
+
#
|
13
|
+
# @example Basic usage
|
14
|
+
# class MyAgentController < ApplicationController
|
15
|
+
# include A2A::Rails::ControllerHelpers
|
16
|
+
#
|
17
|
+
# a2a_skill "greeting" do |skill|
|
18
|
+
# skill.description = "Greet users"
|
19
|
+
# end
|
20
|
+
#
|
21
|
+
# a2a_method "greet" do |params|
|
22
|
+
# { message: "Hello, #{params[:name]}!" }
|
23
|
+
# end
|
24
|
+
# end
|
25
|
+
#
|
26
|
+
module ControllerHelpers
|
27
|
+
extend ActiveSupport::Concern
|
28
|
+
include A2A::Utils::RailsDetection
|
29
|
+
|
30
|
+
included do
|
31
|
+
# Include the A2A Server Agent functionality
|
32
|
+
include A2A::Server::Agent
|
33
|
+
|
34
|
+
# Set up before actions for A2A requests
|
35
|
+
before_action :authenticate_a2a_request, if: :a2a_request?
|
36
|
+
before_action :set_a2a_headers, if: :a2a_request?
|
37
|
+
|
38
|
+
# Skip CSRF protection for A2A endpoints
|
39
|
+
skip_before_action :verify_authenticity_token, if: :a2a_request?
|
40
|
+
|
41
|
+
# Handle A2A-specific exceptions
|
42
|
+
rescue_from A2A::Errors::A2AError, with: :handle_a2a_error
|
43
|
+
rescue_from A2A::Errors::TaskNotFound, with: :handle_task_not_found
|
44
|
+
rescue_from A2A::Errors::AuthenticationError, with: :handle_authentication_error
|
45
|
+
end
|
46
|
+
|
47
|
+
class_methods do
|
48
|
+
##
|
49
|
+
# Configure A2A agent metadata for this controller
|
50
|
+
#
|
51
|
+
# @param options [Hash] Agent configuration options
|
52
|
+
# @option options [String] :name Agent name (defaults to controller name)
|
53
|
+
# @option options [String] :description Agent description
|
54
|
+
# @option options [String] :version Agent version
|
55
|
+
# @option options [Array<String>] :tags Agent tags
|
56
|
+
# @option options [Hash] :metadata Additional metadata
|
57
|
+
#
|
58
|
+
# @example
|
59
|
+
# class ChatController < ApplicationController
|
60
|
+
# include A2A::Rails::ControllerHelpers
|
61
|
+
#
|
62
|
+
# a2a_agent name: "Chat Assistant",
|
63
|
+
# description: "A helpful chat assistant",
|
64
|
+
# version: "1.0.0",
|
65
|
+
# tags: ["chat", "assistant"]
|
66
|
+
# end
|
67
|
+
#
|
68
|
+
def a2a_agent(**options)
|
69
|
+
@_a2a_agent_config = options
|
70
|
+
end
|
71
|
+
|
72
|
+
# Get the A2A agent configuration
|
73
|
+
def a2a_agent_config
|
74
|
+
@a2a_agent_config ||= {}
|
75
|
+
end
|
76
|
+
|
77
|
+
##
|
78
|
+
# Define authentication requirements for A2A methods
|
79
|
+
#
|
80
|
+
# @param methods [Array<String>] Method names that require authentication
|
81
|
+
# @param strategy [Symbol] Authentication strategy (:devise, :jwt, :api_key, :custom)
|
82
|
+
# @param options [Hash] Strategy-specific options
|
83
|
+
#
|
84
|
+
# @example With Devise
|
85
|
+
# a2a_authenticate :devise, methods: ["secure_method"]
|
86
|
+
#
|
87
|
+
# @example With custom strategy
|
88
|
+
# a2a_authenticate :custom, methods: ["secure_method"] do |request|
|
89
|
+
# request.headers["X-API-Key"] == "secret"
|
90
|
+
# end
|
91
|
+
#
|
92
|
+
def a2a_authenticate(strategy = :devise, methods: [], **options, &block)
|
93
|
+
@_a2a_auth_config = {
|
94
|
+
strategy: strategy,
|
95
|
+
methods: Array(methods),
|
96
|
+
options: options,
|
97
|
+
block: block
|
98
|
+
}
|
99
|
+
end
|
100
|
+
|
101
|
+
# Get the A2A authentication configuration
|
102
|
+
def a2a_auth_config
|
103
|
+
@a2a_auth_config ||= { strategy: :none, methods: [], options: {}, block: nil }
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
##
|
108
|
+
# Handle A2A JSON-RPC requests
|
109
|
+
#
|
110
|
+
# This method processes incoming JSON-RPC requests and delegates them to
|
111
|
+
# the appropriate A2A method handlers.
|
112
|
+
#
|
113
|
+
# @return [Hash] JSON-RPC response
|
114
|
+
#
|
115
|
+
def handle_a2a_rpc
|
116
|
+
request_body = request.body.read
|
117
|
+
|
118
|
+
begin
|
119
|
+
json_rpc_request = A2A::Protocol::JsonRpc.parse_request(request_body)
|
120
|
+
|
121
|
+
# Handle batch requests
|
122
|
+
if json_rpc_request.is_a?(Array)
|
123
|
+
responses = json_rpc_request.map { |req| handle_single_a2a_request(req) }
|
124
|
+
render json: responses
|
125
|
+
else
|
126
|
+
response = handle_single_a2a_request(json_rpc_request)
|
127
|
+
render json: response
|
128
|
+
end
|
129
|
+
rescue A2A::Errors::A2AError => e
|
130
|
+
render json: build_a2a_error_response(e), status: :bad_request
|
131
|
+
rescue StandardError => e
|
132
|
+
error = A2A::Errors::InternalError.new(e.message)
|
133
|
+
render json: build_a2a_error_response(error), status: :internal_server_error
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
##
|
138
|
+
# Generate agent card for this controller
|
139
|
+
#
|
140
|
+
# @param authenticated [Boolean] Whether to generate an authenticated card
|
141
|
+
# @return [A2A::Types::AgentCard] The generated agent card
|
142
|
+
#
|
143
|
+
def generate_agent_card(authenticated: false)
|
144
|
+
config = self.class.a2a_agent_config
|
145
|
+
|
146
|
+
# Build base agent card
|
147
|
+
card_data = {
|
148
|
+
name: config[:name] || controller_name.humanize,
|
149
|
+
description: config[:description] || "A2A agent for #{controller_name}",
|
150
|
+
version: config[:version] || "1.0.0",
|
151
|
+
url: agent_card_url,
|
152
|
+
preferred_transport: "JSONRPC",
|
153
|
+
skills: collect_skills,
|
154
|
+
capabilities: collect_capabilities_hash,
|
155
|
+
default_input_modes: A2A.config.default_input_modes,
|
156
|
+
default_output_modes: A2A.config.default_output_modes,
|
157
|
+
additional_interfaces: build_additional_interfaces,
|
158
|
+
security: build_security_config,
|
159
|
+
provider: build_provider_info,
|
160
|
+
protocol_version: A2A.config.protocol_version,
|
161
|
+
supports_authenticated_extended_card: supports_authenticated_card?,
|
162
|
+
documentation_url: documentation_url,
|
163
|
+
metadata: build_agent_metadata(config)
|
164
|
+
}
|
165
|
+
|
166
|
+
# Add authenticated-specific information
|
167
|
+
card_data = enhance_authenticated_card(card_data) if authenticated && current_user_authenticated?
|
168
|
+
|
169
|
+
A2A::Types::AgentCard.new(**card_data)
|
170
|
+
end
|
171
|
+
|
172
|
+
##
|
173
|
+
# Render agent card as JSON response
|
174
|
+
#
|
175
|
+
# @param authenticated [Boolean] Whether to render authenticated card
|
176
|
+
# @param format [Symbol] Response format (:json, :jws)
|
177
|
+
#
|
178
|
+
def render_agent_card(authenticated: false, format: :json)
|
179
|
+
card = generate_agent_card(authenticated: authenticated)
|
180
|
+
|
181
|
+
case format
|
182
|
+
when :json
|
183
|
+
render json: card.to_h
|
184
|
+
when :jws
|
185
|
+
# TODO: Implement JWS signing
|
186
|
+
render json: { error: "JWS format not yet implemented" }, status: :not_implemented
|
187
|
+
else
|
188
|
+
render json: card.to_h
|
189
|
+
end
|
190
|
+
rescue StandardError => e
|
191
|
+
render json: { error: e.message }, status: :internal_server_error
|
192
|
+
end
|
193
|
+
|
194
|
+
##
|
195
|
+
# Check if current request is an A2A request
|
196
|
+
#
|
197
|
+
# @return [Boolean] True if this is an A2A request
|
198
|
+
#
|
199
|
+
def a2a_request?
|
200
|
+
request.path.start_with?(A2A.config.mount_path) ||
|
201
|
+
request.headers["Content-Type"]&.include?("application/json-rpc") ||
|
202
|
+
params[:controller] == "a2a/rails/a2a"
|
203
|
+
end
|
204
|
+
|
205
|
+
##
|
206
|
+
# Check if current user is authenticated for A2A requests
|
207
|
+
#
|
208
|
+
# This method integrates with various Rails authentication systems.
|
209
|
+
#
|
210
|
+
# @return [Boolean] True if user is authenticated
|
211
|
+
#
|
212
|
+
def current_user_authenticated?
|
213
|
+
auth_config = self.class.a2a_auth_config
|
214
|
+
|
215
|
+
case auth_config[:strategy]
|
216
|
+
when :devise
|
217
|
+
respond_to?(:current_user) && current_user.present?
|
218
|
+
when :jwt
|
219
|
+
jwt_authenticated?
|
220
|
+
when :api_key
|
221
|
+
api_key_authenticated?
|
222
|
+
when :custom
|
223
|
+
auth_config[:block]&.call(request) || false
|
224
|
+
else
|
225
|
+
true # No authentication required
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
##
|
230
|
+
# Get current user information for authenticated cards
|
231
|
+
#
|
232
|
+
# @return [Hash] User information hash
|
233
|
+
#
|
234
|
+
def current_user_info
|
235
|
+
if respond_to?(:current_user) && current_user.present?
|
236
|
+
{
|
237
|
+
id: current_user.id,
|
238
|
+
email: current_user.email,
|
239
|
+
name: current_user.name || current_user.email,
|
240
|
+
roles: current_user_roles
|
241
|
+
}
|
242
|
+
else
|
243
|
+
{}
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
##
|
248
|
+
# Get current user permissions for authenticated cards
|
249
|
+
#
|
250
|
+
# @return [Array<String>] List of user permissions
|
251
|
+
#
|
252
|
+
def current_user_permissions
|
253
|
+
if respond_to?(:current_user) && current_user.present?
|
254
|
+
# Try common permission methods
|
255
|
+
if current_user.respond_to?(:permissions)
|
256
|
+
current_user.permissions
|
257
|
+
elsif current_user.respond_to?(:roles)
|
258
|
+
current_user.roles.map(&:name)
|
259
|
+
else
|
260
|
+
[]
|
261
|
+
end
|
262
|
+
else
|
263
|
+
[]
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
private
|
268
|
+
|
269
|
+
def handle_single_a2a_request(json_rpc_request)
|
270
|
+
# Check method-level authentication
|
271
|
+
if method_requires_authentication?(json_rpc_request.method) && !current_user_authenticated?
|
272
|
+
raise A2A::Errors::AuthenticationError, "Authentication required for method: #{json_rpc_request.method}"
|
273
|
+
end
|
274
|
+
|
275
|
+
# Delegate to the A2A request handler from Server::Agent
|
276
|
+
handle_a2a_request(json_rpc_request)
|
277
|
+
rescue A2A::Errors::A2AError => e
|
278
|
+
build_a2a_error_response(e, json_rpc_request.id)
|
279
|
+
rescue StandardError => e
|
280
|
+
error = A2A::Errors::InternalError.new(e.message)
|
281
|
+
build_a2a_error_response(error, json_rpc_request.id)
|
282
|
+
end
|
283
|
+
|
284
|
+
def build_a2a_error_response(error, id = nil)
|
285
|
+
A2A::Protocol::JsonRpc.build_response(
|
286
|
+
error: error.to_json_rpc_error,
|
287
|
+
id: id
|
288
|
+
)
|
289
|
+
end
|
290
|
+
|
291
|
+
def method_requires_authentication?(method_name)
|
292
|
+
auth_config = self.class.a2a_auth_config
|
293
|
+
auth_config[:methods].include?(method_name.to_s)
|
294
|
+
end
|
295
|
+
|
296
|
+
def authenticate_a2a_request
|
297
|
+
return unless A2A.config.authentication_required
|
298
|
+
return if current_user_authenticated?
|
299
|
+
|
300
|
+
raise A2A::Errors::AuthenticationError, "Authentication required"
|
301
|
+
end
|
302
|
+
|
303
|
+
def set_a2a_headers
|
304
|
+
response.headers["X-A2A-Version"] = A2A::VERSION
|
305
|
+
response.headers["X-A2A-Protocol-Version"] = A2A.config.protocol_version
|
306
|
+
response.headers["Content-Type"] = "application/json"
|
307
|
+
end
|
308
|
+
|
309
|
+
def collect_skills
|
310
|
+
capabilities = self.class._a2a_capabilities || []
|
311
|
+
capabilities.map do |capability|
|
312
|
+
{
|
313
|
+
id: capability.name,
|
314
|
+
name: capability.name.humanize,
|
315
|
+
description: capability.description,
|
316
|
+
tags: capability.tags || [],
|
317
|
+
examples: capability.examples || [],
|
318
|
+
input_modes: capability.input_modes || A2A.config.default_input_modes,
|
319
|
+
output_modes: capability.output_modes || A2A.config.default_output_modes
|
320
|
+
}
|
321
|
+
end
|
322
|
+
end
|
323
|
+
|
324
|
+
def collect_capabilities_hash
|
325
|
+
{
|
326
|
+
streaming: A2A.config.streaming_enabled,
|
327
|
+
push_notifications: A2A.config.push_notifications_enabled,
|
328
|
+
state_transition_history: true,
|
329
|
+
extensions: []
|
330
|
+
}
|
331
|
+
end
|
332
|
+
|
333
|
+
def build_additional_interfaces
|
334
|
+
interfaces = []
|
335
|
+
|
336
|
+
# Add gRPC interface if available
|
337
|
+
if defined?(A2A::Transport::Grpc)
|
338
|
+
interfaces << {
|
339
|
+
transport: "GRPC",
|
340
|
+
url: grpc_endpoint_url
|
341
|
+
}
|
342
|
+
end
|
343
|
+
|
344
|
+
# Add HTTP+JSON interface
|
345
|
+
interfaces << {
|
346
|
+
transport: "HTTP+JSON",
|
347
|
+
url: http_json_endpoint_url
|
348
|
+
}
|
349
|
+
|
350
|
+
interfaces
|
351
|
+
end
|
352
|
+
|
353
|
+
def build_security_config
|
354
|
+
auth_config = self.class.a2a_auth_config
|
355
|
+
|
356
|
+
case auth_config[:strategy]
|
357
|
+
when :jwt
|
358
|
+
{
|
359
|
+
security_schemes: {
|
360
|
+
jwt_auth: {
|
361
|
+
type: "http",
|
362
|
+
scheme: "bearer",
|
363
|
+
bearer_format: "JWT"
|
364
|
+
}
|
365
|
+
},
|
366
|
+
security: [{ jwt_auth: [] }]
|
367
|
+
}
|
368
|
+
when :api_key
|
369
|
+
{
|
370
|
+
security_schemes: {
|
371
|
+
api_key_auth: {
|
372
|
+
type: "apiKey",
|
373
|
+
in: "header",
|
374
|
+
name: "X-API-Key"
|
375
|
+
}
|
376
|
+
},
|
377
|
+
security: [{ api_key_auth: [] }]
|
378
|
+
}
|
379
|
+
else
|
380
|
+
{}
|
381
|
+
end
|
382
|
+
end
|
383
|
+
|
384
|
+
def build_provider_info
|
385
|
+
app = rails_application
|
386
|
+
{
|
387
|
+
name: app&.class&.module_parent_name || "Unknown",
|
388
|
+
version: begin
|
389
|
+
app&.config&.version || "1.0.0"
|
390
|
+
rescue StandardError
|
391
|
+
"1.0.0"
|
392
|
+
end,
|
393
|
+
url: root_url
|
394
|
+
}
|
395
|
+
end
|
396
|
+
|
397
|
+
def supports_authenticated_card?
|
398
|
+
self.class.a2a_auth_config[:strategy] != :none
|
399
|
+
end
|
400
|
+
|
401
|
+
def enhance_authenticated_card(card_data)
|
402
|
+
card_data.merge(
|
403
|
+
authenticated_user: current_user_info,
|
404
|
+
permissions: current_user_permissions,
|
405
|
+
authentication_context: {
|
406
|
+
strategy: self.class.a2a_auth_config[:strategy],
|
407
|
+
authenticated_at: Time.now.iso8601
|
408
|
+
}
|
409
|
+
)
|
410
|
+
end
|
411
|
+
|
412
|
+
def build_agent_metadata(config)
|
413
|
+
base_metadata = {
|
414
|
+
controller: controller_name,
|
415
|
+
action: action_name,
|
416
|
+
rails_version: rails_version || "unknown",
|
417
|
+
created_at: Time.now.iso8601
|
418
|
+
}
|
419
|
+
|
420
|
+
base_metadata.merge(config[:metadata] || {})
|
421
|
+
end
|
422
|
+
|
423
|
+
def current_user_roles
|
424
|
+
if respond_to?(:current_user) && current_user.present?
|
425
|
+
if current_user.respond_to?(:roles)
|
426
|
+
current_user.roles.map(&:name)
|
427
|
+
elsif current_user.respond_to?(:role)
|
428
|
+
[current_user.role]
|
429
|
+
else
|
430
|
+
["user"]
|
431
|
+
end
|
432
|
+
else
|
433
|
+
[]
|
434
|
+
end
|
435
|
+
end
|
436
|
+
|
437
|
+
def jwt_authenticated?
|
438
|
+
auth_header = request.headers["Authorization"]
|
439
|
+
return false unless auth_header&.start_with?("Bearer ")
|
440
|
+
|
441
|
+
token = auth_header.split.last
|
442
|
+
|
443
|
+
begin
|
444
|
+
# Basic JWT validation - applications should override this
|
445
|
+
JWT.decode(token, nil, false)
|
446
|
+
true
|
447
|
+
rescue JWT::DecodeError
|
448
|
+
false
|
449
|
+
end
|
450
|
+
end
|
451
|
+
|
452
|
+
def api_key_authenticated?
|
453
|
+
api_key = request.headers["X-API-Key"] || params[:api_key]
|
454
|
+
return false unless api_key.present?
|
455
|
+
|
456
|
+
# Basic API key validation - applications should override this
|
457
|
+
# In a real application, this would check against a database or configuration
|
458
|
+
api_key.length >= 32 # Simple validation
|
459
|
+
end
|
460
|
+
|
461
|
+
# URL helpers for agent card generation
|
462
|
+
def agent_card_url
|
463
|
+
"#{request.base_url}#{A2A.config.mount_path}/agent-card"
|
464
|
+
end
|
465
|
+
|
466
|
+
def grpc_endpoint_url
|
467
|
+
# Convert HTTP URL to gRPC URL (typically different port)
|
468
|
+
base_url = request.base_url.gsub(/:\d+/, ":#{grpc_port}")
|
469
|
+
"#{base_url}/a2a.grpc"
|
470
|
+
end
|
471
|
+
|
472
|
+
def http_json_endpoint_url
|
473
|
+
"#{request.base_url}#{A2A.config.mount_path}/http"
|
474
|
+
end
|
475
|
+
|
476
|
+
def documentation_url
|
477
|
+
"#{request.base_url}/docs/a2a"
|
478
|
+
end
|
479
|
+
|
480
|
+
def grpc_port
|
481
|
+
# Default gRPC port - applications can override this
|
482
|
+
rails_production? ? 443 : 50_051
|
483
|
+
end
|
484
|
+
|
485
|
+
# Exception handlers
|
486
|
+
def handle_a2a_error(error)
|
487
|
+
render json: build_a2a_error_response(error), status: :bad_request
|
488
|
+
end
|
489
|
+
|
490
|
+
def handle_task_not_found(error)
|
491
|
+
render json: build_a2a_error_response(error), status: :not_found
|
492
|
+
end
|
493
|
+
|
494
|
+
def handle_authentication_error(error)
|
495
|
+
render json: build_a2a_error_response(error), status: :unauthorized
|
496
|
+
end
|
497
|
+
end
|
498
|
+
end
|
499
|
+
end
|
@@ -0,0 +1,167 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rails/engine"
|
4
|
+
require_relative "../utils/rails_detection"
|
5
|
+
|
6
|
+
##
|
7
|
+
# Rails Engine for A2A integration
|
8
|
+
#
|
9
|
+
# This engine provides automatic integration with Rails applications,
|
10
|
+
# including middleware setup, route generation, and configuration management.
|
11
|
+
#
|
12
|
+
# @example Basic usage in Rails application
|
13
|
+
# # config/application.rb
|
14
|
+
# require 'a2a/rails'
|
15
|
+
#
|
16
|
+
# class Application < Rails::Application
|
17
|
+
# config.a2a.enabled = true
|
18
|
+
# config.a2a.mount_path = '/a2a'
|
19
|
+
# end
|
20
|
+
#
|
21
|
+
module A2A
|
22
|
+
module Rails
|
23
|
+
class Engine < Rails::Engine
|
24
|
+
extend A2A::Utils::RailsDetection
|
25
|
+
|
26
|
+
isolate_namespace A2A::Rails
|
27
|
+
|
28
|
+
# Configure generators for Rails integration
|
29
|
+
config.generators do |g|
|
30
|
+
g.test_framework :rspec, fixture: false
|
31
|
+
g.fixture_replacement :factory_bot, dir: "spec/factories"
|
32
|
+
g.assets false
|
33
|
+
g.helper false
|
34
|
+
g.stylesheets false
|
35
|
+
g.javascripts false
|
36
|
+
end
|
37
|
+
|
38
|
+
# A2A-specific configuration
|
39
|
+
config.a2a = ActiveSupport::OrderedOptions.new
|
40
|
+
config.a2a.enabled = false
|
41
|
+
config.a2a.mount_path = "/a2a"
|
42
|
+
config.a2a.auto_mount = true
|
43
|
+
config.a2a.middleware_enabled = true
|
44
|
+
config.a2a.authentication_required = false
|
45
|
+
config.a2a.cors_enabled = true
|
46
|
+
config.a2a.rate_limiting_enabled = false
|
47
|
+
config.a2a.logging_enabled = true
|
48
|
+
|
49
|
+
# Initialize A2A configuration
|
50
|
+
initializer "a2a.configuration", before: :load_config_initializers do |app|
|
51
|
+
A2A.configure do |config|
|
52
|
+
config.rails_integration = app.config.a2a.enabled
|
53
|
+
config.mount_path = app.config.a2a.mount_path
|
54
|
+
config.authentication_required = app.config.a2a.authentication_required
|
55
|
+
config.cors_enabled = app.config.a2a.cors_enabled
|
56
|
+
config.rate_limiting_enabled = app.config.a2a.rate_limiting_enabled
|
57
|
+
config.logging_enabled = app.config.a2a.logging_enabled
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# Set up middleware stack
|
62
|
+
initializer "a2a.middleware", after: "a2a.configuration" do |app|
|
63
|
+
if app.config.a2a.enabled && app.config.a2a.middleware_enabled
|
64
|
+
# Add CORS middleware if enabled
|
65
|
+
if app.config.a2a.cors_enabled
|
66
|
+
app.middleware.insert_before ActionDispatch::Static, A2A::Server::Middleware::CorsMiddleware
|
67
|
+
end
|
68
|
+
|
69
|
+
# Add authentication middleware if required
|
70
|
+
app.middleware.use A2A::Server::Middleware::AuthenticationMiddleware if app.config.a2a.authentication_required
|
71
|
+
|
72
|
+
# Add rate limiting middleware if enabled
|
73
|
+
app.middleware.use A2A::Server::Middleware::RateLimitMiddleware if app.config.a2a.rate_limiting_enabled
|
74
|
+
|
75
|
+
# Add logging middleware if enabled
|
76
|
+
app.middleware.use A2A::Server::Middleware::LoggingMiddleware if app.config.a2a.logging_enabled
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# Set up routes
|
81
|
+
initializer "a2a.routes", after: "a2a.middleware" do |app|
|
82
|
+
if app.config.a2a.enabled && app.config.a2a.auto_mount
|
83
|
+
app.routes.prepend do
|
84
|
+
mount A2A::Rails::Engine => app.config.a2a.mount_path
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
# Load A2A helpers into ActionController
|
90
|
+
initializer "a2a.controller_helpers", after: "a2a.routes" do
|
91
|
+
ActiveSupport.on_load(:action_controller_base) do
|
92
|
+
include A2A::Rails::ControllerHelpers if A2A.config.rails_integration
|
93
|
+
end
|
94
|
+
|
95
|
+
ActiveSupport.on_load(:action_controller_api) do
|
96
|
+
include A2A::Rails::ControllerHelpers if A2A.config.rails_integration
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# Configure Rails compatibility
|
101
|
+
initializer "a2a.rails_compatibility" do |_app|
|
102
|
+
# Ensure compatibility with Rails 6.0+
|
103
|
+
if rails_version_supported?
|
104
|
+
# Configure zeitwerk autoloading
|
105
|
+
config.autoload_paths << File.expand_path("..", __dir__)
|
106
|
+
|
107
|
+
# Set up eager loading for production
|
108
|
+
config.eager_load_paths << File.expand_path("..", __dir__) if rails_production?
|
109
|
+
end
|
110
|
+
|
111
|
+
# Configure CSRF protection exemption for A2A endpoints
|
112
|
+
if defined?(ActionController::Base)
|
113
|
+
ActionController::Base.class_eval do
|
114
|
+
protect_from_forgery except: :a2a_rpc, if: -> { A2A.config.rails_integration }
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
# Define engine routes
|
120
|
+
routes.draw do
|
121
|
+
# JSON-RPC endpoint
|
122
|
+
post "/rpc", to: "a2a#rpc", as: :rpc
|
123
|
+
|
124
|
+
# Agent card endpoints
|
125
|
+
get "/agent-card", to: "a2a#agent_card", as: :agent_card
|
126
|
+
get "/capabilities", to: "a2a#capabilities", as: :capabilities
|
127
|
+
|
128
|
+
# Authenticated agent card endpoint
|
129
|
+
get "/authenticated-agent-card", to: "a2a#authenticated_agent_card", as: :authenticated_agent_card
|
130
|
+
|
131
|
+
# Health check endpoint
|
132
|
+
get "/health", to: "a2a#health", as: :health
|
133
|
+
|
134
|
+
# Server-Sent Events endpoint for streaming
|
135
|
+
get "/stream/:task_id", to: "a2a#stream", as: :stream
|
136
|
+
|
137
|
+
# Push notification webhook endpoint
|
138
|
+
post "/webhook/:task_id", to: "a2a#webhook", as: :webhook
|
139
|
+
end
|
140
|
+
|
141
|
+
# Rake tasks
|
142
|
+
rake_tasks do
|
143
|
+
load File.expand_path("tasks/a2a.rake", __dir__)
|
144
|
+
end
|
145
|
+
|
146
|
+
# Generators
|
147
|
+
generators do
|
148
|
+
require_relative "generators/install_generator"
|
149
|
+
require_relative "generators/agent_generator"
|
150
|
+
require_relative "generators/migration_generator"
|
151
|
+
end
|
152
|
+
|
153
|
+
# Validate configuration
|
154
|
+
def self.validate_configuration!(app)
|
155
|
+
unless rails_version_supported?
|
156
|
+
raise A2A::Errors::ConfigurationError,
|
157
|
+
"A2A Rails integration requires Rails 6.0 or higher. Current version: #{rails_version}"
|
158
|
+
end
|
159
|
+
|
160
|
+
return unless app.config.a2a.enabled && !app.config.a2a.mount_path.start_with?("/")
|
161
|
+
|
162
|
+
raise A2A::Errors::ConfigurationError,
|
163
|
+
"A2A mount path must start with '/'. Got: #{app.config.a2a.mount_path}"
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|