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,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ ##
4
+ # Example custom transport plugin
5
+ #
6
+ # Demonstrates how to create a custom transport plugin
7
+ # for the A2A plugin architecture.
8
+ #
9
+ module A2A
10
+ module Plugins
11
+ class ExampleTransport < A2A::Plugin::TransportPlugin
12
+ # Transport name for identification
13
+ def transport_name
14
+ "EXAMPLE"
15
+ end
16
+
17
+ # Send request implementation
18
+ # @param request [Hash] Request data
19
+ # @param **options [Hash] Transport options
20
+ # @return [Hash] Response
21
+ def send_request(request, **_options)
22
+ logger&.info("Sending request via Example Transport: #{request[:method]}")
23
+
24
+ # Simulate request processing
25
+ {
26
+ jsonrpc: "2.0",
27
+ result: { message: "Response from Example Transport" },
28
+ id: request[:id]
29
+ }
30
+ end
31
+
32
+ # This transport supports streaming
33
+ def supports_streaming?
34
+ true
35
+ end
36
+
37
+ # Create streaming connection
38
+ # @param **options [Hash] Connection options
39
+ # @return [Enumerator] Stream enumerator
40
+ def create_stream(**_options)
41
+ Enumerator.new do |yielder|
42
+ 5.times do |i|
43
+ yielder << {
44
+ event: "data",
45
+ data: { message: "Stream message #{i + 1}" }
46
+ }
47
+ sleep(0.1) # Simulate streaming delay
48
+ end
49
+ end
50
+ end
51
+
52
+ # Register hooks for this plugin
53
+ def register_hooks(plugin_manager)
54
+ plugin_manager.add_hook(A2A::Plugin::Events::BEFORE_REQUEST) do |request|
55
+ logger&.debug("Example Transport: Processing request #{request[:id]}")
56
+ request[:transport_metadata] = { plugin: "example_transport" }
57
+ end
58
+
59
+ plugin_manager.add_hook(A2A::Plugin::Events::AFTER_RESPONSE) do |response, request|
60
+ logger&.debug("Example Transport: Processed response for #{request[:id]}")
61
+ response
62
+ end
63
+ end
64
+
65
+ private
66
+
67
+ def setup
68
+ logger&.info("Example Transport plugin initialized")
69
+ end
70
+
71
+ def cleanup
72
+ logger&.info("Example Transport plugin cleaned up")
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Placeholder - will be implemented in task 3.1
4
+ module A2A
5
+ module Protocol
6
+ # Placeholder - will be implemented in task 3.1
7
+ end
8
+ end
@@ -0,0 +1,584 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "time"
4
+ require "json"
5
+
6
+ module A2A
7
+ module Protocol
8
+ ##
9
+ # Serves agent cards with caching and automatic generation
10
+ #
11
+ # The agent card server provides HTTP endpoints for agent card discovery,
12
+ # supports multiple output formats, and includes configurable caching.
13
+ #
14
+ # @example Basic usage
15
+ # server = A2A::Protocol::AgentCardServer.new
16
+ # server.configure do |config|
17
+ # config.cache_ttl = 300 # 5 minutes
18
+ # config.enable_signatures = true
19
+ # end
20
+ #
21
+ # # Register capabilities
22
+ # server.capability_registry.register(capability)
23
+ #
24
+ # # Generate agent card
25
+ # card = server.generate_card(
26
+ # name: "My Agent",
27
+ # description: "A helpful agent"
28
+ # )
29
+ #
30
+ class AgentCardServer
31
+ attr_reader :capability_registry, :config
32
+
33
+ ##
34
+ # Configuration for the agent card server
35
+ #
36
+ class Config
37
+ attr_accessor :cache_ttl, :enable_signatures, :signing_key, :signing_algorithm,
38
+ :default_protocol_version, :enable_authenticated_extended_cards,
39
+ :card_modification_callback
40
+
41
+ def initialize
42
+ @cache_ttl = 300 # 5 minutes
43
+ @enable_signatures = false
44
+ @signing_key = nil
45
+ @signing_algorithm = "RS256"
46
+ @default_protocol_version = "1.0"
47
+ @enable_authenticated_extended_cards = false
48
+ @card_modification_callback = nil
49
+ end
50
+ end
51
+
52
+ ##
53
+ # Initialize a new agent card server
54
+ #
55
+ # @param capability_registry [CapabilityRegistry, nil] Optional registry
56
+ def initialize(capability_registry: nil)
57
+ @capability_registry = capability_registry || CapabilityRegistry.new
58
+ @config = Config.new
59
+ @cache = {}
60
+ @cache_timestamps = {}
61
+ end
62
+
63
+ ##
64
+ # Configure the server
65
+ #
66
+ # @yield [config] Configuration block
67
+ # @yieldparam config [Config] The configuration object
68
+ def configure
69
+ yield(@config) if block_given?
70
+ end
71
+
72
+ ##
73
+ # Generate an agent card from registered capabilities
74
+ #
75
+ # @param name [String] Agent name
76
+ # @param description [String] Agent description
77
+ # @param version [String] Agent version
78
+ # @param url [String] Primary agent URL
79
+ # @param preferred_transport [String] Preferred transport
80
+ # @param additional_options [Hash] Additional card options
81
+ # @return [A2A::Types::AgentCard] The generated agent card
82
+ def generate_card(name:, description:, url:, version: "1.0.0",
83
+ preferred_transport: "JSONRPC", **additional_options)
84
+ # Convert capabilities to skills
85
+ skills = @capability_registry.all.map { |cap| capability_to_skill(cap) }
86
+
87
+ # Determine capabilities from registry
88
+ capabilities = determine_capabilities
89
+
90
+ # Build default input/output modes from skills
91
+ input_modes = determine_input_modes(skills)
92
+ output_modes = determine_output_modes(skills)
93
+
94
+ # Create the agent card
95
+ card_options = {
96
+ name: name,
97
+ description: description,
98
+ version: version,
99
+ url: url,
100
+ preferred_transport: preferred_transport,
101
+ skills: skills,
102
+ capabilities: capabilities,
103
+ default_input_modes: input_modes,
104
+ default_output_modes: output_modes,
105
+ protocol_version: @config.default_protocol_version,
106
+ supports_authenticated_extended_card: @config.enable_authenticated_extended_cards
107
+ }.merge(additional_options)
108
+
109
+ card = A2A::Types::AgentCard.new(**card_options)
110
+
111
+ # Add signatures if enabled
112
+ if @config.enable_signatures && @config.signing_key
113
+ signatures = [generate_signature(card)]
114
+ card = A2A::Types::AgentCard.new(**card_options, signatures: signatures)
115
+ end
116
+
117
+ card
118
+ end
119
+
120
+ ##
121
+ # Get agent card with caching
122
+ #
123
+ # @param cache_key [String] Cache key for the card
124
+ # @param card_params [Hash] Parameters for card generation
125
+ # @return [A2A::Types::AgentCard] The agent card
126
+ def get_card(cache_key: "default", **card_params)
127
+ # Check cache
128
+ if (cached_card = get_from_cache(cache_key))
129
+ return cached_card
130
+ end
131
+
132
+ # Generate new card
133
+ card = generate_card(**card_params)
134
+
135
+ # Cache the card
136
+ store_in_cache(cache_key, card)
137
+
138
+ card
139
+ end
140
+
141
+ ##
142
+ # Get authenticated extended agent card
143
+ #
144
+ # This method allows for dynamic modification of the agent card
145
+ # based on the authentication context.
146
+ #
147
+ # @param auth_context [Hash] Authentication context
148
+ # @param base_card_params [Hash] Base card parameters
149
+ # @return [A2A::Types::AgentCard] The extended agent card
150
+ def get_authenticated_extended_card(auth_context: {}, **base_card_params)
151
+ unless @config.enable_authenticated_extended_cards
152
+ raise A2A::Errors::A2AError.new(
153
+ "Authenticated extended cards are not enabled",
154
+ code: -32_001
155
+ )
156
+ end
157
+
158
+ # Generate base card
159
+ card = generate_card(**base_card_params)
160
+
161
+ # Apply modifications based on auth context
162
+ card = @config.card_modification_callback.call(card, auth_context) if @config.card_modification_callback
163
+
164
+ card
165
+ end
166
+
167
+ ##
168
+ # Serve agent card as HTTP response data
169
+ #
170
+ # @param format [String] Output format ('json' or 'jws')
171
+ # @param cache_key [String] Cache key
172
+ # @param card_params [Hash] Card generation parameters
173
+ # @return [Hash] HTTP response data with headers and body
174
+ def serve_card(format: "json", cache_key: "default", **card_params)
175
+ card = get_card(cache_key: cache_key, **card_params)
176
+
177
+ case format.downcase
178
+ when "json"
179
+ {
180
+ status: 200,
181
+ headers: {
182
+ "Content-Type" => "application/json",
183
+ "Cache-Control" => "max-age=#{@config.cache_ttl}"
184
+ },
185
+ body: card.to_json
186
+ }
187
+ when "jws"
188
+ if @config.enable_signatures && @config.signing_key
189
+ jws_token = generate_jws_token(card)
190
+ {
191
+ status: 200,
192
+ headers: {
193
+ "Content-Type" => "application/jose+json",
194
+ "Cache-Control" => "max-age=#{@config.cache_ttl}"
195
+ },
196
+ body: jws_token
197
+ }
198
+ else
199
+ {
200
+ status: 400,
201
+ headers: { "Content-Type" => "application/json" },
202
+ body: { error: "JWS signing not configured" }.to_json
203
+ }
204
+ end
205
+ else
206
+ {
207
+ status: 400,
208
+ headers: { "Content-Type" => "application/json" },
209
+ body: { error: "Unsupported format: #{format}" }.to_json
210
+ }
211
+ end
212
+ end
213
+
214
+ ##
215
+ # Clear cache
216
+ #
217
+ # @param cache_key [String, nil] Specific key to clear, or nil for all
218
+ def clear_cache(cache_key = nil)
219
+ if cache_key
220
+ @cache.delete(cache_key)
221
+ @cache_timestamps.delete(cache_key)
222
+ else
223
+ @cache.clear
224
+ @cache_timestamps.clear
225
+ end
226
+ end
227
+
228
+ ##
229
+ # Get cache statistics
230
+ #
231
+ # @return [Hash] Cache statistics
232
+ def cache_stats
233
+ {
234
+ entries: @cache.size,
235
+ keys: @cache.keys,
236
+ oldest_entry: @cache_timestamps.values.min,
237
+ newest_entry: @cache_timestamps.values.max
238
+ }
239
+ end
240
+
241
+ private
242
+
243
+ ##
244
+ # Convert a capability to an agent skill
245
+ #
246
+ # @param capability [Capability] The capability to convert
247
+ # @return [A2A::Types::AgentSkill] The converted skill
248
+ def capability_to_skill(capability)
249
+ A2A::Types::AgentSkill.new(
250
+ id: capability.name,
251
+ name: capability.name.split("_").map(&:capitalize).join(" "),
252
+ description: capability.description,
253
+ tags: capability.tags,
254
+ examples: capability.examples,
255
+ input_modes: determine_capability_input_modes(capability),
256
+ output_modes: determine_capability_output_modes(capability),
257
+ security: capability.security_requirements
258
+ )
259
+ end
260
+
261
+ ##
262
+ # Determine agent capabilities from registry
263
+ #
264
+ # @return [A2A::Types::AgentCapabilities] The agent capabilities
265
+ def determine_capabilities
266
+ streaming = @capability_registry.all.any?(&:streaming?)
267
+ push_notifications = false # TODO: Determine from server config
268
+ state_history = false # TODO: Determine from server config
269
+ extensions = [] # TODO: Collect from capabilities
270
+
271
+ A2A::Types::AgentCapabilities.new(
272
+ streaming: streaming,
273
+ push_notifications: push_notifications,
274
+ state_transition_history: state_history,
275
+ extensions: extensions
276
+ )
277
+ end
278
+
279
+ ##
280
+ # Determine input modes from skills
281
+ #
282
+ # @param skills [Array<A2A::Types::AgentSkill>] The skills
283
+ # @return [Array<String>] Input modes
284
+ def determine_input_modes(skills)
285
+ modes = skills.flat_map { |skill| skill.input_modes || ["text"] }.uniq
286
+ modes.empty? ? ["text"] : modes
287
+ end
288
+
289
+ ##
290
+ # Determine output modes from skills
291
+ #
292
+ # @param skills [Array<A2A::Types::AgentSkill>] The skills
293
+ # @return [Array<String>] Output modes
294
+ def determine_output_modes(skills)
295
+ modes = skills.flat_map { |skill| skill.output_modes || ["text"] }.uniq
296
+ modes.empty? ? ["text"] : modes
297
+ end
298
+
299
+ ##
300
+ # Determine input modes for a capability
301
+ #
302
+ # @param capability [Capability] The capability
303
+ # @return [Array<String>] Input modes
304
+ def determine_capability_input_modes(capability)
305
+ # Analyze input schema to determine modes
306
+ return ["text"] unless capability.input_schema
307
+
308
+ modes = ["text"] # Default to text
309
+
310
+ # Check if schema accepts file inputs
311
+ modes << "file" if schema_accepts_files?(capability.input_schema)
312
+
313
+ # Check if schema accepts structured data
314
+ modes << "data" if schema_accepts_data?(capability.input_schema)
315
+
316
+ modes.uniq
317
+ end
318
+
319
+ ##
320
+ # Determine output modes for a capability
321
+ #
322
+ # @param capability [Capability] The capability
323
+ # @return [Array<String>] Output modes
324
+ def determine_capability_output_modes(capability)
325
+ # Analyze output schema to determine modes
326
+ return ["text"] unless capability.output_schema
327
+
328
+ modes = ["text"] # Default to text
329
+
330
+ # Check if schema produces files
331
+ modes << "file" if schema_produces_files?(capability.output_schema)
332
+
333
+ # Check if schema produces structured data
334
+ modes << "data" if schema_produces_data?(capability.output_schema)
335
+
336
+ modes.uniq
337
+ end
338
+
339
+ ##
340
+ # Check if schema accepts file inputs
341
+ #
342
+ # @param schema [Hash] The JSON schema
343
+ # @return [Boolean] True if files are accepted
344
+ def schema_accepts_files?(schema)
345
+ # Simple heuristic: look for file-related properties
346
+ return false unless schema.is_a?(Hash)
347
+
348
+ properties = schema[:properties] || schema["properties"] || {}
349
+ properties.any? do |name, prop_schema|
350
+ name.to_s.include?("file") ||
351
+ (prop_schema.is_a?(Hash) && prop_schema[:format] == "binary")
352
+ end
353
+ end
354
+
355
+ ##
356
+ # Check if schema accepts structured data
357
+ #
358
+ # @param schema [Hash] The JSON schema
359
+ # @return [Boolean] True if structured data is accepted
360
+ def schema_accepts_data?(schema)
361
+ return false unless schema.is_a?(Hash)
362
+
363
+ type = schema[:type] || schema["type"]
364
+ %w[object array].include?(type)
365
+ end
366
+
367
+ ##
368
+ # Check if schema produces files
369
+ #
370
+ # @param schema [Hash] The JSON schema
371
+ # @return [Boolean] True if files are produced
372
+ def schema_produces_files?(schema)
373
+ schema_accepts_files?(schema) # Same logic for now
374
+ end
375
+
376
+ ##
377
+ # Check if schema produces structured data
378
+ #
379
+ # @param schema [Hash] The JSON schema
380
+ # @return [Boolean] True if structured data is produced
381
+ def schema_produces_data?(schema)
382
+ schema_accepts_data?(schema) # Same logic for now
383
+ end
384
+
385
+ ##
386
+ # Get card from cache if valid
387
+ #
388
+ # @param cache_key [String] The cache key
389
+ # @return [A2A::Types::AgentCard, nil] Cached card or nil
390
+ def get_from_cache(cache_key)
391
+ return nil unless @cache.key?(cache_key)
392
+
393
+ timestamp = @cache_timestamps[cache_key]
394
+ return nil unless timestamp
395
+
396
+ # Check if cache entry is still valid
397
+ if Time.now - timestamp < @config.cache_ttl
398
+ @cache[cache_key]
399
+ else
400
+ # Remove expired entry
401
+ @cache.delete(cache_key)
402
+ @cache_timestamps.delete(cache_key)
403
+ nil
404
+ end
405
+ end
406
+
407
+ ##
408
+ # Store card in cache
409
+ #
410
+ # @param cache_key [String] The cache key
411
+ # @param card [A2A::Types::AgentCard] The card to cache
412
+ def store_in_cache(cache_key, card)
413
+ @cache[cache_key] = card
414
+ @cache_timestamps[cache_key] = Time.now
415
+ end
416
+
417
+ ##
418
+ # Generate a JWS signature for the agent card
419
+ #
420
+ # @param card [A2A::Types::AgentCard] The card to sign
421
+ # @return [A2A::Types::AgentCardSignature] The signature
422
+ def generate_signature(_card)
423
+ # This is a placeholder implementation
424
+ # In a real implementation, you would use a proper JWT library
425
+
426
+ header = {
427
+ alg: @config.signing_algorithm,
428
+ typ: "JWT"
429
+ }
430
+
431
+ require "base64"
432
+ header_b64 = Base64.urlsafe_encode64(header.to_json).delete("=")
433
+
434
+ # Placeholder signature (in real implementation, sign with private key)
435
+ signature_b64 = Base64.urlsafe_encode64("signature_#{Time.now.to_i}").delete("=")
436
+
437
+ A2A::Types::AgentCardSignature.new(
438
+ signature: signature_b64,
439
+ protected_header: header_b64
440
+ )
441
+ end
442
+
443
+ ##
444
+ # Generate a complete JWS token for the agent card
445
+ #
446
+ # @param card [A2A::Types::AgentCard] The card to sign
447
+ # @return [String] The JWS token
448
+ def generate_jws_token(card)
449
+ # This is a placeholder implementation
450
+ # In a real implementation, you would use a proper JWT library
451
+
452
+ header = {
453
+ alg: @config.signing_algorithm,
454
+ typ: "JWT"
455
+ }
456
+
457
+ payload = card.to_h
458
+
459
+ require "base64"
460
+ header_b64 = Base64.urlsafe_encode64(header.to_json).delete("=")
461
+ payload_b64 = Base64.urlsafe_encode64(payload.to_json).delete("=")
462
+
463
+ # Placeholder signature (in real implementation, sign with private key)
464
+ signature_b64 = Base64.urlsafe_encode64("signature_#{Time.now.to_i}").delete("=")
465
+
466
+ "#{header_b64}.#{payload_b64}.#{signature_b64}"
467
+ end
468
+ end
469
+
470
+ ##
471
+ # HTTP endpoint handlers for agent card serving
472
+ #
473
+ # Provides Rack-compatible handlers for serving agent cards
474
+ # over HTTP with proper content negotiation and caching.
475
+ #
476
+ class AgentCardEndpoints
477
+ def initialize(server)
478
+ @server = server
479
+ end
480
+
481
+ ##
482
+ # Handle agent card requests
483
+ #
484
+ # @param env [Hash] Rack environment
485
+ # @return [Array] Rack response [status, headers, body]
486
+ def call(env)
487
+ Rack::Request.new(env) if defined?(Rack::Request)
488
+
489
+ # Simple request parsing for non-Rack environments
490
+ method = env["REQUEST_METHOD"] || "GET"
491
+ path = env["PATH_INFO"] || env["REQUEST_URI"] || "/"
492
+ query_params = parse_query_string(env["QUERY_STRING"] || "")
493
+
494
+ case path
495
+ when "/agent-card", "/agent-card.json"
496
+ handle_agent_card_request(method, query_params)
497
+ when "/agent-card.jws"
498
+ handle_agent_card_jws_request(method, query_params)
499
+ when "/capabilities"
500
+ handle_capabilities_request(method, query_params)
501
+ else
502
+ [404, { "Content-Type" => "application/json" }, ['{"error":"Not found"}']]
503
+ end
504
+ end
505
+
506
+ private
507
+
508
+ ##
509
+ # Handle agent card JSON requests
510
+ def handle_agent_card_request(method, params)
511
+ return method_not_allowed unless method == "GET"
512
+
513
+ response = @server.serve_card(
514
+ format: "json",
515
+ cache_key: params["cache_key"] || "default",
516
+ name: params["name"] || "Agent",
517
+ description: params["description"] || "An A2A agent",
518
+ version: params["version"] || "1.0.0",
519
+ url: params["url"] || "https://example.com/agent"
520
+ )
521
+
522
+ [response[:status], response[:headers], [response[:body]]]
523
+ end
524
+
525
+ ##
526
+ # Handle agent card JWS requests
527
+ def handle_agent_card_jws_request(method, params)
528
+ return method_not_allowed unless method == "GET"
529
+
530
+ response = @server.serve_card(
531
+ format: "jws",
532
+ cache_key: params["cache_key"] || "default",
533
+ name: params["name"] || "Agent",
534
+ description: params["description"] || "An A2A agent",
535
+ version: params["version"] || "1.0.0",
536
+ url: params["url"] || "https://example.com/agent"
537
+ )
538
+
539
+ [response[:status], response[:headers], [response[:body]]]
540
+ end
541
+
542
+ ##
543
+ # Handle capabilities listing requests
544
+ def handle_capabilities_request(method, _params)
545
+ return method_not_allowed unless method == "GET"
546
+
547
+ capabilities = @server.capability_registry.to_h
548
+
549
+ [
550
+ 200,
551
+ { "Content-Type" => "application/json" },
552
+ [capabilities.to_json]
553
+ ]
554
+ end
555
+
556
+ ##
557
+ # Parse query string into hash
558
+ def parse_query_string(query_string)
559
+ return {} if query_string.empty?
560
+
561
+ params = {}
562
+ query_string.split("&").each do |pair|
563
+ key, value = pair.split("=", 2)
564
+ next unless key
565
+
566
+ key = URI.decode_www_form_component(key) if defined?(URI.decode_www_form_component)
567
+ value = URI.decode_www_form_component(value || "") if defined?(URI.decode_www_form_component)
568
+ params[key] = value
569
+ end
570
+ params
571
+ end
572
+
573
+ ##
574
+ # Return method not allowed response
575
+ def method_not_allowed
576
+ [
577
+ 405,
578
+ { "Content-Type" => "application/json" },
579
+ ['{"error":"Method not allowed"}']
580
+ ]
581
+ end
582
+ end
583
+ end
584
+ end