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.
Files changed (128) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +137 -0
  4. data/.simplecov +46 -0
  5. data/.yardopts +10 -0
  6. data/CHANGELOG.md +33 -0
  7. data/CODE_OF_CONDUCT.md +128 -0
  8. data/CONTRIBUTING.md +165 -0
  9. data/Gemfile +43 -0
  10. data/Guardfile +34 -0
  11. data/LICENSE.txt +21 -0
  12. data/PUBLISHING_CHECKLIST.md +214 -0
  13. data/README.md +171 -0
  14. data/Rakefile +165 -0
  15. data/docs/agent_execution.md +309 -0
  16. data/docs/api_reference.md +792 -0
  17. data/docs/configuration.md +780 -0
  18. data/docs/events.md +475 -0
  19. data/docs/getting_started.md +668 -0
  20. data/docs/integration.md +262 -0
  21. data/docs/server_apps.md +621 -0
  22. data/docs/troubleshooting.md +765 -0
  23. data/lib/a2a/client/api_methods.rb +263 -0
  24. data/lib/a2a/client/auth/api_key.rb +161 -0
  25. data/lib/a2a/client/auth/interceptor.rb +288 -0
  26. data/lib/a2a/client/auth/jwt.rb +189 -0
  27. data/lib/a2a/client/auth/oauth2.rb +146 -0
  28. data/lib/a2a/client/auth.rb +137 -0
  29. data/lib/a2a/client/base.rb +316 -0
  30. data/lib/a2a/client/config.rb +210 -0
  31. data/lib/a2a/client/connection_pool.rb +233 -0
  32. data/lib/a2a/client/http_client.rb +524 -0
  33. data/lib/a2a/client/json_rpc_handler.rb +136 -0
  34. data/lib/a2a/client/middleware/circuit_breaker_interceptor.rb +245 -0
  35. data/lib/a2a/client/middleware/logging_interceptor.rb +371 -0
  36. data/lib/a2a/client/middleware/rate_limit_interceptor.rb +142 -0
  37. data/lib/a2a/client/middleware/retry_interceptor.rb +161 -0
  38. data/lib/a2a/client/middleware.rb +116 -0
  39. data/lib/a2a/client/performance_tracker.rb +60 -0
  40. data/lib/a2a/configuration/defaults.rb +34 -0
  41. data/lib/a2a/configuration/environment_loader.rb +76 -0
  42. data/lib/a2a/configuration/file_loader.rb +115 -0
  43. data/lib/a2a/configuration/inheritance.rb +101 -0
  44. data/lib/a2a/configuration/validator.rb +180 -0
  45. data/lib/a2a/configuration.rb +201 -0
  46. data/lib/a2a/errors.rb +291 -0
  47. data/lib/a2a/modules.rb +50 -0
  48. data/lib/a2a/monitoring/alerting.rb +490 -0
  49. data/lib/a2a/monitoring/distributed_tracing.rb +398 -0
  50. data/lib/a2a/monitoring/health_endpoints.rb +204 -0
  51. data/lib/a2a/monitoring/metrics_collector.rb +438 -0
  52. data/lib/a2a/monitoring.rb +463 -0
  53. data/lib/a2a/plugin.rb +358 -0
  54. data/lib/a2a/plugin_manager.rb +159 -0
  55. data/lib/a2a/plugins/example_auth.rb +81 -0
  56. data/lib/a2a/plugins/example_middleware.rb +118 -0
  57. data/lib/a2a/plugins/example_transport.rb +76 -0
  58. data/lib/a2a/protocol/agent_card.rb +8 -0
  59. data/lib/a2a/protocol/agent_card_server.rb +584 -0
  60. data/lib/a2a/protocol/capability.rb +496 -0
  61. data/lib/a2a/protocol/json_rpc.rb +254 -0
  62. data/lib/a2a/protocol/message.rb +8 -0
  63. data/lib/a2a/protocol/task.rb +8 -0
  64. data/lib/a2a/rails/a2a_controller.rb +258 -0
  65. data/lib/a2a/rails/controller_helpers.rb +499 -0
  66. data/lib/a2a/rails/engine.rb +167 -0
  67. data/lib/a2a/rails/generators/agent_generator.rb +311 -0
  68. data/lib/a2a/rails/generators/install_generator.rb +209 -0
  69. data/lib/a2a/rails/generators/migration_generator.rb +232 -0
  70. data/lib/a2a/rails/generators/templates/add_a2a_indexes.rb +57 -0
  71. data/lib/a2a/rails/generators/templates/agent_controller.rb +122 -0
  72. data/lib/a2a/rails/generators/templates/agent_controller_spec.rb +160 -0
  73. data/lib/a2a/rails/generators/templates/agent_readme.md +200 -0
  74. data/lib/a2a/rails/generators/templates/create_a2a_push_notification_configs.rb +68 -0
  75. data/lib/a2a/rails/generators/templates/create_a2a_tasks.rb +83 -0
  76. data/lib/a2a/rails/generators/templates/example_agent_controller.rb +228 -0
  77. data/lib/a2a/rails/generators/templates/initializer.rb +108 -0
  78. data/lib/a2a/rails/generators/templates/push_notification_config_model.rb +228 -0
  79. data/lib/a2a/rails/generators/templates/task_model.rb +200 -0
  80. data/lib/a2a/rails/tasks/a2a.rake +228 -0
  81. data/lib/a2a/server/a2a_methods.rb +520 -0
  82. data/lib/a2a/server/agent.rb +537 -0
  83. data/lib/a2a/server/agent_execution/agent_executor.rb +279 -0
  84. data/lib/a2a/server/agent_execution/request_context.rb +219 -0
  85. data/lib/a2a/server/apps/rack_app.rb +311 -0
  86. data/lib/a2a/server/apps/sinatra_app.rb +261 -0
  87. data/lib/a2a/server/default_request_handler.rb +350 -0
  88. data/lib/a2a/server/events/event_consumer.rb +116 -0
  89. data/lib/a2a/server/events/event_queue.rb +226 -0
  90. data/lib/a2a/server/example_agent.rb +248 -0
  91. data/lib/a2a/server/handler.rb +281 -0
  92. data/lib/a2a/server/middleware/authentication_middleware.rb +212 -0
  93. data/lib/a2a/server/middleware/cors_middleware.rb +171 -0
  94. data/lib/a2a/server/middleware/logging_middleware.rb +362 -0
  95. data/lib/a2a/server/middleware/rate_limit_middleware.rb +382 -0
  96. data/lib/a2a/server/middleware.rb +213 -0
  97. data/lib/a2a/server/push_notification_manager.rb +327 -0
  98. data/lib/a2a/server/request_handler.rb +136 -0
  99. data/lib/a2a/server/storage/base.rb +141 -0
  100. data/lib/a2a/server/storage/database.rb +266 -0
  101. data/lib/a2a/server/storage/memory.rb +274 -0
  102. data/lib/a2a/server/storage/redis.rb +320 -0
  103. data/lib/a2a/server/storage.rb +38 -0
  104. data/lib/a2a/server/task_manager.rb +534 -0
  105. data/lib/a2a/transport/grpc.rb +481 -0
  106. data/lib/a2a/transport/http.rb +415 -0
  107. data/lib/a2a/transport/sse.rb +499 -0
  108. data/lib/a2a/types/agent_card.rb +540 -0
  109. data/lib/a2a/types/artifact.rb +99 -0
  110. data/lib/a2a/types/base_model.rb +223 -0
  111. data/lib/a2a/types/events.rb +117 -0
  112. data/lib/a2a/types/message.rb +106 -0
  113. data/lib/a2a/types/part.rb +288 -0
  114. data/lib/a2a/types/push_notification.rb +139 -0
  115. data/lib/a2a/types/security.rb +167 -0
  116. data/lib/a2a/types/task.rb +154 -0
  117. data/lib/a2a/types.rb +88 -0
  118. data/lib/a2a/utils/helpers.rb +245 -0
  119. data/lib/a2a/utils/message_buffer.rb +278 -0
  120. data/lib/a2a/utils/performance.rb +247 -0
  121. data/lib/a2a/utils/rails_detection.rb +97 -0
  122. data/lib/a2a/utils/structured_logger.rb +306 -0
  123. data/lib/a2a/utils/time_helpers.rb +167 -0
  124. data/lib/a2a/utils/validation.rb +8 -0
  125. data/lib/a2a/version.rb +6 -0
  126. data/lib/a2a-rails.rb +58 -0
  127. data/lib/a2a.rb +198 -0
  128. 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