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,212 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "base64"
4
+ require_relative "../../errors"
5
+
6
+ module A2A
7
+ module Server
8
+ module Middleware
9
+ end
10
+ end
11
+ end
12
+
13
+ ##
14
+ # Authentication middleware for A2A requests
15
+ #
16
+ # Handles authentication for A2A requests based on various security schemes
17
+ # including OAuth2, JWT, API keys, and HTTP authentication.
18
+ #
19
+ # @example Basic usage
20
+ # middleware = AuthenticationMiddleware.new(
21
+ # schemes: ['bearer', 'api_key'],
22
+ # required: true
23
+ # )
24
+ #
25
+ module A2A
26
+ module Server
27
+ module Middleware
28
+ class AuthenticationMiddleware
29
+ attr_reader :schemes, :required, :authenticators
30
+
31
+ ##
32
+ # Initialize authentication middleware
33
+ #
34
+ # @param schemes [Array<String>] Allowed authentication schemes
35
+ # @param required [Boolean] Whether authentication is required
36
+ # @param authenticators [Hash] Custom authenticator implementations
37
+ def initialize(schemes: [], required: false, authenticators: {})
38
+ @schemes = schemes.map(&:to_s)
39
+ @required = required
40
+ @authenticators = default_authenticators.merge(authenticators)
41
+ end
42
+
43
+ ##
44
+ # Process authentication for a request
45
+ #
46
+ # @param request [A2A::Protocol::Request] The JSON-RPC request
47
+ # @param context [A2A::Server::Context] The request context
48
+ # @yield Block to continue the middleware chain
49
+ # @return [Object] The result from the next middleware or handler
50
+ def call(request, context)
51
+ # Extract authentication information
52
+ auth_info = extract_authentication(request, context)
53
+
54
+ if auth_info
55
+ scheme, credentials = auth_info
56
+
57
+ # Check if scheme is allowed
58
+ unless @schemes.empty? || @schemes.include?(scheme)
59
+ raise A2A::Errors::AuthorizationFailed, "Authentication scheme '#{scheme}' not supported"
60
+ end
61
+
62
+ # Authenticate using the appropriate authenticator
63
+ authenticate(scheme, credentials, context)
64
+
65
+ elsif @required
66
+ raise A2A::Errors::AuthenticationRequired,
67
+ "Authentication required. Supported schemes: #{@schemes.join(', ')}"
68
+ end
69
+
70
+ # Continue to next middleware
71
+ yield
72
+ end
73
+
74
+ ##
75
+ # Add a custom authenticator
76
+ #
77
+ # @param scheme [String] The authentication scheme name
78
+ # @param authenticator [Proc] The authenticator proc
79
+ def add_authenticator(scheme, authenticator)
80
+ @authenticators[scheme.to_s] = authenticator
81
+ end
82
+
83
+ private
84
+
85
+ ##
86
+ # Extract authentication information from request/context
87
+ #
88
+ # @param request [A2A::Protocol::Request] The request
89
+ # @param context [A2A::Server::Context] The context
90
+ # @return [Array<String>, nil] [scheme, credentials] or nil if no auth
91
+ def extract_authentication(request, context)
92
+ # Try to extract from various sources
93
+
94
+ # 1. Check context metadata for HTTP headers
95
+ auth_header = context.get_metadata(:authorization) ||
96
+ context.get_metadata("Authorization")
97
+
98
+ return parse_authorization_header(auth_header) if auth_header
99
+
100
+ # 2. Check request parameters for API key
101
+ if request.params.is_a?(Hash)
102
+ api_key = request.params["api_key"] || request.params[:api_key]
103
+ return ["api_key", api_key] if api_key
104
+ end
105
+
106
+ # 3. Check context for pre-set authentication
107
+ if context.authenticated?
108
+ # Return the first available authentication scheme
109
+ context.instance_variable_get(:@auth_schemes)&.first
110
+ end
111
+
112
+ nil
113
+ end
114
+
115
+ ##
116
+ # Parse Authorization header
117
+ #
118
+ # @param header [String] The Authorization header value
119
+ # @return [Array<String>] [scheme, credentials]
120
+ def parse_authorization_header(header)
121
+ parts = header.strip.split(" ", 2)
122
+ return nil if parts.length != 2
123
+
124
+ scheme = parts[0].downcase
125
+ credentials = parts[1]
126
+
127
+ [scheme, credentials]
128
+ end
129
+
130
+ ##
131
+ # Authenticate using the appropriate authenticator
132
+ #
133
+ # @param scheme [String] The authentication scheme
134
+ # @param credentials [String] The credentials
135
+ # @param context [A2A::Server::Context] The request context
136
+ def authenticate(scheme, credentials, context)
137
+ authenticator = @authenticators[scheme]
138
+
139
+ unless authenticator
140
+ raise A2A::Errors::AuthorizationFailed,
141
+ "No authenticator configured for scheme '#{scheme}'"
142
+ end
143
+
144
+ # Call the authenticator
145
+ result = authenticator.call(credentials, context)
146
+
147
+ raise A2A::Errors::AuthorizationFailed, "Authentication failed for scheme '#{scheme}'" unless result
148
+
149
+ # Set authentication in context
150
+ context.set_authentication(scheme, result)
151
+ end
152
+
153
+ ##
154
+ # Default authenticator implementations
155
+ #
156
+ # @return [Hash] Hash of scheme name to authenticator proc
157
+ def default_authenticators
158
+ {
159
+ "bearer" => method(:authenticate_bearer),
160
+ "basic" => method(:authenticate_basic),
161
+ "api_key" => method(:authenticate_api_key)
162
+ }
163
+ end
164
+
165
+ ##
166
+ # Authenticate Bearer token (JWT or API key)
167
+ #
168
+ # @param token [String] The bearer token
169
+ # @param context [A2A::Server::Context] The request context
170
+ # @return [Hash, nil] Authentication result or nil if invalid
171
+ def authenticate_bearer(token, _context)
172
+ # This is a basic implementation - in practice you would:
173
+ # 1. Validate JWT signature and expiration
174
+ # 2. Check API key against database
175
+ # 3. Extract user/session information
176
+
177
+ # For now, just return the token as valid
178
+ { token: token, scheme: "bearer" }
179
+ end
180
+
181
+ ##
182
+ # Authenticate Basic authentication
183
+ #
184
+ # @param credentials [String] Base64 encoded username:password
185
+ # @param context [A2A::Server::Context] The request context
186
+ # @return [Hash, nil] Authentication result or nil if invalid
187
+ def authenticate_basic(credentials, _context)
188
+ decoded = Base64.decode64(credentials)
189
+ username, = decoded.split(":", 2)
190
+
191
+ # In practice, validate against user database
192
+ # For now, just return the username
193
+ { username: username, scheme: "basic" }
194
+ rescue StandardError
195
+ nil
196
+ end
197
+
198
+ ##
199
+ # Authenticate API key
200
+ #
201
+ # @param api_key [String] The API key
202
+ # @param context [A2A::Server::Context] The request context
203
+ # @return [Hash, nil] Authentication result or nil if invalid
204
+ def authenticate_api_key(api_key, _context)
205
+ # In practice, validate against API key database
206
+ # For now, just return the key as valid
207
+ { api_key: api_key, scheme: "api_key" }
208
+ end
209
+ end
210
+ end
211
+ end
212
+ end
@@ -0,0 +1,171 @@
1
+ # frozen_string_literal: true
2
+
3
+ ##
4
+ # CORS (Cross-Origin Resource Sharing) middleware for A2A requests
5
+ #
6
+ # Handles CORS headers for A2A JSON-RPC requests to enable
7
+ # cross-origin requests from web browsers.
8
+ #
9
+ # @example Basic usage
10
+ # middleware = CorsMiddleware.new(
11
+ # origins: ['https://example.com'],
12
+ # methods: ['POST', 'OPTIONS'],
13
+ # headers: ['Content-Type', 'Authorization']
14
+ # )
15
+ #
16
+ module A2A
17
+ module Server
18
+ module Middleware
19
+ class CorsMiddleware
20
+ attr_reader :origins, :methods, :headers, :credentials, :max_age
21
+
22
+ ##
23
+ # Initialize CORS middleware
24
+ #
25
+ # @param origins [Array<String>, String] Allowed origins (* for all)
26
+ # @param methods [Array<String>] Allowed HTTP methods
27
+ # @param headers [Array<String>] Allowed headers
28
+ # @param credentials [Boolean] Whether to allow credentials
29
+ # @param max_age [Integer] Preflight cache duration in seconds
30
+ # @param expose_headers [Array<String>] Headers to expose to client
31
+ def initialize(origins: "*", methods: %w[POST OPTIONS],
32
+ headers: %w[Content-Type Authorization],
33
+ credentials: false, max_age: 86_400, expose_headers: [])
34
+ @origins = normalize_origins(origins)
35
+ @methods = Array(methods).map(&:upcase)
36
+ @headers = Array(headers)
37
+ @credentials = credentials
38
+ @max_age = max_age
39
+ @expose_headers = Array(expose_headers)
40
+ end
41
+
42
+ ##
43
+ # Process CORS for a request
44
+ #
45
+ # @param request [A2A::Protocol::Request] The JSON-RPC request
46
+ # @param context [A2A::Server::Context] The request context
47
+ # @yield Block to continue the middleware chain
48
+ # @return [Object] The result from the next middleware or handler
49
+ def call(_request, context)
50
+ # Extract HTTP method and origin from context
51
+ http_method = context.get_metadata(:http_method) || "POST"
52
+ origin = context.get_metadata(:origin) || context.get_metadata("Origin")
53
+
54
+ # Handle preflight requests
55
+ return handle_preflight(origin, context) if http_method.casecmp("OPTIONS").zero?
56
+
57
+ # Add CORS headers to the response
58
+ add_cors_headers(origin, context)
59
+
60
+ # Continue to next middleware
61
+ yield
62
+ end
63
+
64
+ ##
65
+ # Check if an origin is allowed
66
+ #
67
+ # @param origin [String] The origin to check
68
+ # @return [Boolean] True if origin is allowed
69
+ def origin_allowed?(origin)
70
+ return true if @origins == "*"
71
+ return false if origin.nil?
72
+
73
+ @origins.any? do |allowed_origin|
74
+ if allowed_origin.include?("*")
75
+ # Handle wildcard patterns
76
+ pattern = Regexp.escape(allowed_origin).gsub('\*', ".*")
77
+ origin.match?(/\A#{pattern}\z/)
78
+ else
79
+ origin == allowed_origin
80
+ end
81
+ end
82
+ end
83
+
84
+ ##
85
+ # Get CORS headers for an origin
86
+ #
87
+ # @param origin [String] The request origin
88
+ # @return [Hash] CORS headers
89
+ def cors_headers(origin)
90
+ headers = {}
91
+
92
+ if origin_allowed?(origin)
93
+ headers["Access-Control-Allow-Origin"] = @origins == "*" ? "*" : origin
94
+
95
+ headers["Access-Control-Allow-Credentials"] = "true" if @credentials
96
+
97
+ headers["Access-Control-Expose-Headers"] = @expose_headers.join(", ") unless @expose_headers.empty?
98
+ end
99
+
100
+ headers
101
+ end
102
+
103
+ ##
104
+ # Get preflight CORS headers
105
+ #
106
+ # @param origin [String] The request origin
107
+ # @return [Hash] Preflight CORS headers
108
+ def preflight_headers(origin)
109
+ headers = cors_headers(origin)
110
+
111
+ if origin_allowed?(origin)
112
+ headers["Access-Control-Allow-Methods"] = @methods.join(", ")
113
+ headers["Access-Control-Allow-Headers"] = @headers.join(", ")
114
+ headers["Access-Control-Max-Age"] = @max_age.to_s
115
+ end
116
+
117
+ headers
118
+ end
119
+
120
+ private
121
+
122
+ ##
123
+ # Normalize origins configuration
124
+ #
125
+ # @param origins [Array, String] Origins configuration
126
+ # @return [Array<String>, String] Normalized origins
127
+ def normalize_origins(origins)
128
+ case origins
129
+ when String
130
+ origins == "*" ? "*" : [origins]
131
+ when Array
132
+ origins.include?("*") ? "*" : origins
133
+ else
134
+ ["*"]
135
+ end
136
+ end
137
+
138
+ ##
139
+ # Handle preflight OPTIONS request
140
+ #
141
+ # @param origin [String] The request origin
142
+ # @param context [A2A::Server::Context] The request context
143
+ # @return [Hash] Preflight response
144
+ def handle_preflight(origin, context)
145
+ # Add preflight headers to context for response
146
+ preflight_headers(origin).each do |key, value|
147
+ context.set_metadata("response_header_#{key.downcase}", value)
148
+ end
149
+
150
+ # Return empty response for preflight
151
+ {
152
+ status: 200,
153
+ headers: preflight_headers(origin),
154
+ body: ""
155
+ }
156
+ end
157
+
158
+ ##
159
+ # Add CORS headers to response context
160
+ #
161
+ # @param origin [String] The request origin
162
+ # @param context [A2A::Server::Context] The request context
163
+ def add_cors_headers(origin, context)
164
+ cors_headers(origin).each do |key, value|
165
+ context.set_metadata("response_header_#{key.downcase}", value)
166
+ end
167
+ end
168
+ end
169
+ end
170
+ end
171
+ end