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,382 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../../errors"
|
4
|
+
|
5
|
+
module A2A
|
6
|
+
module Server
|
7
|
+
module Middleware
|
8
|
+
##
|
9
|
+
# Rate limiting middleware for A2A requests
|
10
|
+
#
|
11
|
+
# Implements rate limiting using various strategies including
|
12
|
+
# in-memory, Redis-backed, and sliding window algorithms.
|
13
|
+
#
|
14
|
+
# @example Basic usage
|
15
|
+
# middleware = RateLimitMiddleware.new(
|
16
|
+
# limit: 100,
|
17
|
+
# window: 3600, # 1 hour
|
18
|
+
# strategy: :sliding_window
|
19
|
+
# )
|
20
|
+
#
|
21
|
+
class RateLimitMiddleware
|
22
|
+
attr_reader :limit, :window, :strategy, :store
|
23
|
+
|
24
|
+
# Rate limiting strategies
|
25
|
+
STRATEGIES = %i[fixed_window sliding_window token_bucket].freeze
|
26
|
+
|
27
|
+
##
|
28
|
+
# Initialize rate limiting middleware
|
29
|
+
#
|
30
|
+
# @param limit [Integer] Maximum number of requests per window
|
31
|
+
# @param window [Integer] Time window in seconds
|
32
|
+
# @param strategy [Symbol] Rate limiting strategy
|
33
|
+
# @param store [Object] Storage backend (defaults to in-memory)
|
34
|
+
# @param key_generator [Proc] Custom key generator for rate limiting
|
35
|
+
def initialize(limit: 100, window: 3600, strategy: :sliding_window,
|
36
|
+
store: nil, key_generator: nil)
|
37
|
+
@limit = limit
|
38
|
+
@window = window
|
39
|
+
@strategy = strategy
|
40
|
+
@store = store || InMemoryStore.new
|
41
|
+
@key_generator = key_generator || method(:default_key_generator)
|
42
|
+
|
43
|
+
validate_strategy!
|
44
|
+
end
|
45
|
+
|
46
|
+
##
|
47
|
+
# Process rate limiting for a request
|
48
|
+
#
|
49
|
+
# @param request [A2A::Protocol::Request] The JSON-RPC request
|
50
|
+
# @param context [A2A::Server::Context] The request context
|
51
|
+
# @yield Block to continue the middleware chain
|
52
|
+
# @return [Object] The result from the next middleware or handler
|
53
|
+
# @raise [A2A::Errors::RateLimitExceeded] If rate limit is exceeded
|
54
|
+
def call(request, context)
|
55
|
+
# Generate rate limiting key
|
56
|
+
key = @key_generator.call(request, context)
|
57
|
+
|
58
|
+
# Check rate limit
|
59
|
+
unless check_rate_limit(key)
|
60
|
+
raise A2A::Errors::RateLimitExceeded, "Rate limit exceeded: #{@limit} requests per #{@window} seconds"
|
61
|
+
end
|
62
|
+
|
63
|
+
# Continue to next middleware
|
64
|
+
yield
|
65
|
+
end
|
66
|
+
|
67
|
+
##
|
68
|
+
# Check if request is within rate limit
|
69
|
+
#
|
70
|
+
# @param key [String] The rate limiting key
|
71
|
+
# @return [Boolean] True if within limit, false otherwise
|
72
|
+
def check_rate_limit(key)
|
73
|
+
case @strategy
|
74
|
+
when :fixed_window
|
75
|
+
check_fixed_window(key)
|
76
|
+
when :sliding_window
|
77
|
+
check_sliding_window(key)
|
78
|
+
when :token_bucket
|
79
|
+
check_token_bucket(key)
|
80
|
+
else
|
81
|
+
true # Fallback to allow request
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
##
|
86
|
+
# Get current rate limit status for a key
|
87
|
+
#
|
88
|
+
# @param key [String] The rate limiting key
|
89
|
+
# @return [Hash] Status information
|
90
|
+
def status(key)
|
91
|
+
case @strategy
|
92
|
+
when :fixed_window
|
93
|
+
fixed_window_status(key)
|
94
|
+
when :sliding_window
|
95
|
+
sliding_window_status(key)
|
96
|
+
when :token_bucket
|
97
|
+
token_bucket_status(key)
|
98
|
+
else
|
99
|
+
{ limit: @limit, remaining: @limit, reset_time: nil }
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
private
|
104
|
+
|
105
|
+
##
|
106
|
+
# Validate the rate limiting strategy
|
107
|
+
def validate_strategy!
|
108
|
+
return if STRATEGIES.include?(@strategy)
|
109
|
+
|
110
|
+
raise ArgumentError, "Invalid strategy: #{@strategy}. Must be one of: #{STRATEGIES.join(', ')}"
|
111
|
+
end
|
112
|
+
|
113
|
+
##
|
114
|
+
# Default key generator based on authentication or IP
|
115
|
+
#
|
116
|
+
# @param request [A2A::Protocol::Request] The request
|
117
|
+
# @param context [A2A::Server::Context] The context
|
118
|
+
# @return [String] The rate limiting key
|
119
|
+
def default_key_generator(_request, context)
|
120
|
+
# Try to use authenticated user/API key
|
121
|
+
if context.authenticated?
|
122
|
+
auth_data = context.instance_variable_get(:@auth_schemes)&.values&.first
|
123
|
+
return "user:#{auth_data[:username] || auth_data[:api_key] || auth_data[:token]}" if auth_data.is_a?(Hash)
|
124
|
+
end
|
125
|
+
|
126
|
+
# Fall back to IP address if available
|
127
|
+
ip = context.get_metadata(:remote_ip) || context.get_metadata("REMOTE_ADDR")
|
128
|
+
return "ip:#{ip}" if ip
|
129
|
+
|
130
|
+
# Default fallback
|
131
|
+
"anonymous"
|
132
|
+
end
|
133
|
+
|
134
|
+
##
|
135
|
+
# Fixed window rate limiting
|
136
|
+
#
|
137
|
+
# @param key [String] The rate limiting key
|
138
|
+
# @return [Boolean] True if within limit
|
139
|
+
def check_fixed_window(key)
|
140
|
+
now = Time.now.to_i
|
141
|
+
window_start = (now / @window) * @window
|
142
|
+
window_key = "#{key}:#{window_start}"
|
143
|
+
|
144
|
+
current_count = @store.get(window_key) || 0
|
145
|
+
|
146
|
+
if current_count >= @limit
|
147
|
+
false
|
148
|
+
else
|
149
|
+
@store.increment(window_key, expires_at: window_start + @window)
|
150
|
+
true
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
##
|
155
|
+
# Sliding window rate limiting
|
156
|
+
#
|
157
|
+
# @param key [String] The rate limiting key
|
158
|
+
# @return [Boolean] True if within limit
|
159
|
+
def check_sliding_window(key)
|
160
|
+
now = Time.now.to_f
|
161
|
+
window_start = now - @window
|
162
|
+
|
163
|
+
# Get timestamps of requests in the current window
|
164
|
+
timestamps = @store.get_list("#{key}:timestamps") || []
|
165
|
+
|
166
|
+
# Remove old timestamps
|
167
|
+
timestamps = timestamps.select { |ts| ts > window_start }
|
168
|
+
|
169
|
+
if timestamps.length >= @limit
|
170
|
+
false
|
171
|
+
else
|
172
|
+
# Add current timestamp
|
173
|
+
timestamps << now
|
174
|
+
@store.set_list("#{key}:timestamps", timestamps, expires_at: now + @window)
|
175
|
+
true
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
##
|
180
|
+
# Token bucket rate limiting
|
181
|
+
#
|
182
|
+
# @param key [String] The rate limiting key
|
183
|
+
# @return [Boolean] True if within limit
|
184
|
+
def check_token_bucket(key)
|
185
|
+
now = Time.now.to_f
|
186
|
+
bucket_key = "#{key}:bucket"
|
187
|
+
|
188
|
+
# Get current bucket state
|
189
|
+
bucket = @store.get(bucket_key) || { tokens: @limit, last_refill: now }
|
190
|
+
|
191
|
+
# Calculate tokens to add based on time elapsed
|
192
|
+
time_elapsed = now - bucket[:last_refill]
|
193
|
+
tokens_to_add = (time_elapsed / @window) * @limit
|
194
|
+
|
195
|
+
# Refill bucket
|
196
|
+
bucket[:tokens] = [@limit, bucket[:tokens] + tokens_to_add].min
|
197
|
+
bucket[:last_refill] = now
|
198
|
+
|
199
|
+
if bucket[:tokens] >= 1
|
200
|
+
bucket[:tokens] -= 1
|
201
|
+
@store.set(bucket_key, bucket, expires_at: now + (@window * 2))
|
202
|
+
true
|
203
|
+
else
|
204
|
+
@store.set(bucket_key, bucket, expires_at: now + (@window * 2))
|
205
|
+
false
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
##
|
210
|
+
# Get fixed window status
|
211
|
+
def fixed_window_status(key)
|
212
|
+
now = Time.now.to_i
|
213
|
+
window_start = (now / @window) * @window
|
214
|
+
window_key = "#{key}:#{window_start}"
|
215
|
+
|
216
|
+
current_count = @store.get(window_key) || 0
|
217
|
+
reset_time = window_start + @window
|
218
|
+
|
219
|
+
{
|
220
|
+
limit: @limit,
|
221
|
+
remaining: [@limit - current_count, 0].max,
|
222
|
+
reset_time: Time.zone.at(reset_time),
|
223
|
+
window_start: Time.zone.at(window_start)
|
224
|
+
}
|
225
|
+
end
|
226
|
+
|
227
|
+
##
|
228
|
+
# Get sliding window status
|
229
|
+
def sliding_window_status(key)
|
230
|
+
now = Time.now.to_f
|
231
|
+
window_start = now - @window
|
232
|
+
|
233
|
+
timestamps = @store.get_list("#{key}:timestamps") || []
|
234
|
+
current_count = timestamps.count { |ts| ts > window_start }
|
235
|
+
|
236
|
+
{
|
237
|
+
limit: @limit,
|
238
|
+
remaining: [@limit - current_count, 0].max,
|
239
|
+
reset_time: nil, # No fixed reset time for sliding window
|
240
|
+
window_start: Time.zone.at(window_start)
|
241
|
+
}
|
242
|
+
end
|
243
|
+
|
244
|
+
##
|
245
|
+
# Get token bucket status
|
246
|
+
def token_bucket_status(key)
|
247
|
+
now = Time.now.to_f
|
248
|
+
bucket_key = "#{key}:bucket"
|
249
|
+
|
250
|
+
bucket = @store.get(bucket_key) || { tokens: @limit, last_refill: now }
|
251
|
+
|
252
|
+
# Calculate current tokens
|
253
|
+
time_elapsed = now - bucket[:last_refill]
|
254
|
+
tokens_to_add = (time_elapsed / @window) * @limit
|
255
|
+
current_tokens = [@limit, bucket[:tokens] + tokens_to_add].min
|
256
|
+
|
257
|
+
{
|
258
|
+
limit: @limit,
|
259
|
+
remaining: current_tokens.floor,
|
260
|
+
reset_time: nil, # Continuous refill
|
261
|
+
tokens: current_tokens
|
262
|
+
}
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
##
|
267
|
+
# In-memory storage for rate limiting
|
268
|
+
#
|
269
|
+
class InMemoryStore
|
270
|
+
def initialize
|
271
|
+
@data = {}
|
272
|
+
@mutex = Mutex.new
|
273
|
+
end
|
274
|
+
|
275
|
+
def get(key)
|
276
|
+
@mutex.synchronize do
|
277
|
+
entry = @data[key]
|
278
|
+
return nil unless entry
|
279
|
+
|
280
|
+
# Check expiration
|
281
|
+
if entry[:expires_at] && Time.now.to_f > entry[:expires_at]
|
282
|
+
@data.delete(key)
|
283
|
+
return nil
|
284
|
+
end
|
285
|
+
|
286
|
+
entry[:value]
|
287
|
+
end
|
288
|
+
end
|
289
|
+
|
290
|
+
def set(key, value, expires_at: nil)
|
291
|
+
@mutex.synchronize do
|
292
|
+
@data[key] = {
|
293
|
+
value: value,
|
294
|
+
expires_at: expires_at
|
295
|
+
}
|
296
|
+
end
|
297
|
+
end
|
298
|
+
|
299
|
+
def increment(key, expires_at: nil)
|
300
|
+
@mutex.synchronize do
|
301
|
+
entry = @data[key] || { value: 0, expires_at: expires_at }
|
302
|
+
|
303
|
+
# Check expiration
|
304
|
+
entry = { value: 0, expires_at: expires_at } if entry[:expires_at] && Time.now.to_f > entry[:expires_at]
|
305
|
+
|
306
|
+
entry[:value] += 1
|
307
|
+
entry[:expires_at] = expires_at if expires_at
|
308
|
+
@data[key] = entry
|
309
|
+
|
310
|
+
entry[:value]
|
311
|
+
end
|
312
|
+
end
|
313
|
+
|
314
|
+
def get_list(key)
|
315
|
+
get(key)
|
316
|
+
end
|
317
|
+
|
318
|
+
def set_list(key, list, expires_at: nil)
|
319
|
+
set(key, list, expires_at: expires_at)
|
320
|
+
end
|
321
|
+
|
322
|
+
def clear
|
323
|
+
@mutex.synchronize do
|
324
|
+
@data.clear
|
325
|
+
end
|
326
|
+
end
|
327
|
+
|
328
|
+
def size
|
329
|
+
@mutex.synchronize do
|
330
|
+
@data.size
|
331
|
+
end
|
332
|
+
end
|
333
|
+
end
|
334
|
+
|
335
|
+
##
|
336
|
+
# Redis-backed storage for rate limiting
|
337
|
+
#
|
338
|
+
class RedisStore
|
339
|
+
def initialize(redis_client)
|
340
|
+
@redis = redis_client
|
341
|
+
end
|
342
|
+
|
343
|
+
def get(key)
|
344
|
+
value = @redis.get(key)
|
345
|
+
value ? JSON.parse(value) : nil
|
346
|
+
rescue JSON::ParserError
|
347
|
+
nil
|
348
|
+
end
|
349
|
+
|
350
|
+
def set(key, value, expires_at: nil)
|
351
|
+
json_value = JSON.generate(value)
|
352
|
+
|
353
|
+
if expires_at
|
354
|
+
ttl = (expires_at - Time.now.to_f).ceil
|
355
|
+
@redis.setex(key, ttl, json_value) if ttl.positive?
|
356
|
+
else
|
357
|
+
@redis.set(key, json_value)
|
358
|
+
end
|
359
|
+
end
|
360
|
+
|
361
|
+
def increment(key, expires_at: nil)
|
362
|
+
result = @redis.incr(key)
|
363
|
+
|
364
|
+
if expires_at && result == 1
|
365
|
+
ttl = (expires_at - Time.now.to_f).ceil
|
366
|
+
@redis.expire(key, ttl) if ttl.positive?
|
367
|
+
end
|
368
|
+
|
369
|
+
result
|
370
|
+
end
|
371
|
+
|
372
|
+
def get_list(key)
|
373
|
+
get(key)
|
374
|
+
end
|
375
|
+
|
376
|
+
def set_list(key, list, expires_at: nil)
|
377
|
+
set(key, list, expires_at: expires_at)
|
378
|
+
end
|
379
|
+
end
|
380
|
+
end
|
381
|
+
end
|
382
|
+
end
|
@@ -0,0 +1,213 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "middleware/authentication_middleware"
|
4
|
+
require_relative "middleware/rate_limit_middleware"
|
5
|
+
require_relative "middleware/logging_middleware"
|
6
|
+
require_relative "middleware/cors_middleware"
|
7
|
+
|
8
|
+
##
|
9
|
+
# Server middleware for A2A request processing
|
10
|
+
#
|
11
|
+
# This module provides various middleware components for A2A servers,
|
12
|
+
# including authentication, rate limiting, logging, and CORS support.
|
13
|
+
#
|
14
|
+
# @example Using middleware with a handler
|
15
|
+
# handler = A2A::Server::Handler.new(agent)
|
16
|
+
# handler.add_middleware(A2A::Server::Middleware::AuthenticationMiddleware.new)
|
17
|
+
# handler.add_middleware(A2A::Server::Middleware::LoggingMiddleware.new)
|
18
|
+
#
|
19
|
+
module A2A
|
20
|
+
module Server
|
21
|
+
module Middleware
|
22
|
+
##
|
23
|
+
# Middleware registry for managing middleware instances
|
24
|
+
#
|
25
|
+
class Registry
|
26
|
+
def initialize
|
27
|
+
@middleware = []
|
28
|
+
end
|
29
|
+
|
30
|
+
##
|
31
|
+
# Add middleware to the registry
|
32
|
+
#
|
33
|
+
# @param middleware [Object] Middleware instance
|
34
|
+
# @param options [Hash] Middleware options
|
35
|
+
def add(middleware, **options)
|
36
|
+
@middleware << { instance: middleware, options: options }
|
37
|
+
end
|
38
|
+
|
39
|
+
##
|
40
|
+
# Remove middleware from the registry
|
41
|
+
#
|
42
|
+
# @param middleware [Object] Middleware instance to remove
|
43
|
+
def remove(middleware)
|
44
|
+
@middleware.reject! { |m| m[:instance] == middleware }
|
45
|
+
end
|
46
|
+
|
47
|
+
##
|
48
|
+
# Get all middleware instances
|
49
|
+
#
|
50
|
+
# @return [Array] Array of middleware instances
|
51
|
+
def all
|
52
|
+
@middleware.pluck(:instance)
|
53
|
+
end
|
54
|
+
|
55
|
+
##
|
56
|
+
# Clear all middleware
|
57
|
+
def clear
|
58
|
+
@middleware.clear
|
59
|
+
end
|
60
|
+
|
61
|
+
##
|
62
|
+
# Get middleware count
|
63
|
+
#
|
64
|
+
# @return [Integer] Number of registered middleware
|
65
|
+
def count
|
66
|
+
@middleware.size
|
67
|
+
end
|
68
|
+
|
69
|
+
##
|
70
|
+
# Check if middleware is registered
|
71
|
+
#
|
72
|
+
# @param middleware [Object] Middleware instance to check
|
73
|
+
# @return [Boolean] True if registered
|
74
|
+
def include?(middleware)
|
75
|
+
@middleware.any? { |m| m[:instance] == middleware }
|
76
|
+
end
|
77
|
+
|
78
|
+
##
|
79
|
+
# Execute middleware chain
|
80
|
+
#
|
81
|
+
# @param request [A2A::Protocol::Request] The request
|
82
|
+
# @param context [A2A::Server::Context] The request context
|
83
|
+
# @yield Block to execute after all middleware
|
84
|
+
# @return [Object] Result from the block
|
85
|
+
def call(request, context, &block)
|
86
|
+
chain = block
|
87
|
+
|
88
|
+
# Build middleware chain from the end backwards
|
89
|
+
@middleware.reverse_each do |middleware_def|
|
90
|
+
middleware = middleware_def[:instance]
|
91
|
+
current_chain = chain
|
92
|
+
|
93
|
+
chain = lambda do
|
94
|
+
if middleware.respond_to?(:call)
|
95
|
+
middleware.call(request, context) { current_chain.call }
|
96
|
+
else
|
97
|
+
current_chain.call
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
# Execute the chain
|
103
|
+
chain.call
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
##
|
108
|
+
# Middleware builder for creating middleware stacks
|
109
|
+
#
|
110
|
+
class Builder
|
111
|
+
def initialize
|
112
|
+
@registry = Registry.new
|
113
|
+
end
|
114
|
+
|
115
|
+
##
|
116
|
+
# Add authentication middleware
|
117
|
+
#
|
118
|
+
# @param options [Hash] Authentication options
|
119
|
+
def use_authentication(**options)
|
120
|
+
@registry.add(AuthenticationMiddleware.new(**options))
|
121
|
+
self
|
122
|
+
end
|
123
|
+
|
124
|
+
##
|
125
|
+
# Add rate limiting middleware
|
126
|
+
#
|
127
|
+
# @param options [Hash] Rate limiting options
|
128
|
+
def use_rate_limiting(**options)
|
129
|
+
@registry.add(RateLimitMiddleware.new(**options))
|
130
|
+
self
|
131
|
+
end
|
132
|
+
|
133
|
+
##
|
134
|
+
# Add logging middleware
|
135
|
+
#
|
136
|
+
# @param options [Hash] Logging options
|
137
|
+
def use_logging(**options)
|
138
|
+
@registry.add(LoggingMiddleware.new(**options))
|
139
|
+
self
|
140
|
+
end
|
141
|
+
|
142
|
+
##
|
143
|
+
# Add CORS middleware
|
144
|
+
#
|
145
|
+
# @param options [Hash] CORS options
|
146
|
+
def use_cors(**options)
|
147
|
+
@registry.add(CorsMiddleware.new(**options))
|
148
|
+
self
|
149
|
+
end
|
150
|
+
|
151
|
+
##
|
152
|
+
# Add custom middleware
|
153
|
+
#
|
154
|
+
# @param middleware [Object] Middleware instance
|
155
|
+
# @param options [Hash] Middleware options
|
156
|
+
def use(middleware, **options)
|
157
|
+
@registry.add(middleware, **options)
|
158
|
+
self
|
159
|
+
end
|
160
|
+
|
161
|
+
##
|
162
|
+
# Build the middleware registry
|
163
|
+
#
|
164
|
+
# @return [Registry] The built middleware registry
|
165
|
+
def build
|
166
|
+
@registry
|
167
|
+
end
|
168
|
+
|
169
|
+
##
|
170
|
+
# Execute the middleware stack
|
171
|
+
#
|
172
|
+
# @param request [A2A::Protocol::Request] The request
|
173
|
+
# @param context [A2A::Server::Context] The request context
|
174
|
+
# @yield Block to execute after all middleware
|
175
|
+
# @return [Object] Result from the block
|
176
|
+
def call(request, context, &block)
|
177
|
+
@registry.call(request, context, &block)
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
##
|
182
|
+
# Create a new middleware builder
|
183
|
+
#
|
184
|
+
# @return [Builder] New middleware builder instance
|
185
|
+
def self.build
|
186
|
+
Builder.new
|
187
|
+
end
|
188
|
+
|
189
|
+
##
|
190
|
+
# Create a middleware stack with common middleware
|
191
|
+
#
|
192
|
+
# @param options [Hash] Configuration options
|
193
|
+
# @return [Registry] Configured middleware registry
|
194
|
+
def self.default_stack(**options)
|
195
|
+
builder = build
|
196
|
+
|
197
|
+
# Add logging by default
|
198
|
+
builder.use_logging(options[:logging] || {})
|
199
|
+
|
200
|
+
# Add authentication if configured
|
201
|
+
builder.use_authentication(options[:authentication]) if options[:authentication]
|
202
|
+
|
203
|
+
# Add rate limiting if configured
|
204
|
+
builder.use_rate_limiting(options[:rate_limiting]) if options[:rate_limiting]
|
205
|
+
|
206
|
+
# Add CORS if configured
|
207
|
+
builder.use_cors(options[:cors]) if options[:cors]
|
208
|
+
|
209
|
+
builder.build
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|