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,254 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
|
5
|
+
# Try to use Oj for faster JSON parsing if available
|
6
|
+
begin
|
7
|
+
require "oj"
|
8
|
+
OJ_AVAILABLE = true
|
9
|
+
rescue LoadError
|
10
|
+
OJ_AVAILABLE = false
|
11
|
+
end
|
12
|
+
|
13
|
+
module A2A
|
14
|
+
module Protocol
|
15
|
+
##
|
16
|
+
# JSON-RPC 2.0 implementation for A2A protocol
|
17
|
+
#
|
18
|
+
# This class provides parsing and building functionality for JSON-RPC 2.0 requests
|
19
|
+
# and responses, including support for batch requests and proper error handling.
|
20
|
+
# Optimized for performance with optional Oj JSON parser support.
|
21
|
+
#
|
22
|
+
# @see https://www.jsonrpc.org/specification JSON-RPC 2.0 Specification
|
23
|
+
class JsonRpc
|
24
|
+
# JSON-RPC 2.0 version string
|
25
|
+
JSONRPC_VERSION = "2.0"
|
26
|
+
|
27
|
+
# Standard JSON-RPC error codes
|
28
|
+
PARSE_ERROR = -32_700
|
29
|
+
INVALID_REQUEST = -32_600
|
30
|
+
METHOD_NOT_FOUND = -32_601
|
31
|
+
INVALID_PARAMS = -32_602
|
32
|
+
INTERNAL_ERROR = -32_603
|
33
|
+
|
34
|
+
# A2A-specific error codes
|
35
|
+
TASK_NOT_FOUND = -32_001
|
36
|
+
TASK_NOT_CANCELABLE = -32_002
|
37
|
+
INVALID_TASK_STATE = -32_003
|
38
|
+
AUTHENTICATION_REQUIRED = -32_004
|
39
|
+
AUTHORIZATION_FAILED = -32_005
|
40
|
+
RATE_LIMIT_EXCEEDED = -32_006
|
41
|
+
AGENT_UNAVAILABLE = -32_007
|
42
|
+
PROTOCOL_VERSION_MISMATCH = -32_008
|
43
|
+
CAPABILITY_NOT_SUPPORTED = -32_009
|
44
|
+
RESOURCE_EXHAUSTED = -32_010
|
45
|
+
|
46
|
+
##
|
47
|
+
# Parse JSON string using optimized parser if available
|
48
|
+
#
|
49
|
+
# @param json_string [String] JSON string to parse
|
50
|
+
# @return [Hash, Array] Parsed JSON data
|
51
|
+
def self.parse_json(json_string)
|
52
|
+
if OJ_AVAILABLE
|
53
|
+
Oj.load(json_string, mode: :strict, symbol_keys: false)
|
54
|
+
else
|
55
|
+
JSON.parse(json_string)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
##
|
60
|
+
# Generate JSON string using optimized generator if available
|
61
|
+
#
|
62
|
+
# @param object [Object] Object to serialize
|
63
|
+
# @return [String] JSON string
|
64
|
+
def self.generate_json(object)
|
65
|
+
if OJ_AVAILABLE
|
66
|
+
Oj.dump(object, mode: :compat)
|
67
|
+
else
|
68
|
+
JSON.generate(object)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
##
|
73
|
+
# Parse a JSON-RPC 2.0 request from JSON string
|
74
|
+
#
|
75
|
+
# @param json_string [String] The JSON string to parse
|
76
|
+
# @return [Request, Array<Request>] Single request or array for batch requests
|
77
|
+
# @raise [A2A::Errors::ParseError] If JSON is invalid
|
78
|
+
# @raise [A2A::Errors::InvalidRequest] If request format is invalid
|
79
|
+
def self.parse_request(json_string)
|
80
|
+
# Performance optimization: early return for empty strings
|
81
|
+
return nil if json_string.nil? || (json_string.respond_to?(:empty?) && json_string.empty?)
|
82
|
+
|
83
|
+
# Ensure we have a string
|
84
|
+
json_string = json_string.to_s unless json_string.is_a?(String)
|
85
|
+
|
86
|
+
begin
|
87
|
+
# Use optimized JSON parser if available
|
88
|
+
parsed = parse_json(json_string)
|
89
|
+
rescue JSON::ParserError, Oj::ParseError => e
|
90
|
+
raise A2A::Errors::ParseError, "Invalid JSON: #{e.message}"
|
91
|
+
end
|
92
|
+
|
93
|
+
if parsed.is_a?(Array)
|
94
|
+
# Batch request
|
95
|
+
raise A2A::Errors::InvalidRequest, "Empty batch request" if parsed.empty?
|
96
|
+
|
97
|
+
# Performance optimization: use map! for in-place modification
|
98
|
+
parsed.map! { |req| parse_single_request(req) }
|
99
|
+
else
|
100
|
+
# Single request
|
101
|
+
parse_single_request(parsed)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
##
|
106
|
+
# Build a JSON-RPC 2.0 response
|
107
|
+
#
|
108
|
+
# @param result [Object, nil] The result value (mutually exclusive with error)
|
109
|
+
# @param error [Hash, nil] The error object (mutually exclusive with result)
|
110
|
+
# @param id [String, Integer, nil] The request ID
|
111
|
+
# @return [Hash] The response hash
|
112
|
+
def self.build_response(id:, **kwargs)
|
113
|
+
result_provided = kwargs.key?(:result)
|
114
|
+
error_provided = kwargs.key?(:error)
|
115
|
+
|
116
|
+
raise ArgumentError, "Cannot specify both result and error" if result_provided && error_provided
|
117
|
+
raise ArgumentError, "Must specify either result or error" unless result_provided || error_provided
|
118
|
+
|
119
|
+
response = {
|
120
|
+
jsonrpc: JSONRPC_VERSION,
|
121
|
+
id: id
|
122
|
+
}
|
123
|
+
|
124
|
+
if error_provided
|
125
|
+
response[:error] = normalize_error(kwargs[:error])
|
126
|
+
else
|
127
|
+
response[:result] = kwargs[:result]
|
128
|
+
end
|
129
|
+
|
130
|
+
response
|
131
|
+
end
|
132
|
+
|
133
|
+
##
|
134
|
+
# Build a JSON-RPC 2.0 batch response
|
135
|
+
#
|
136
|
+
# @param responses [Array<Hash>] Array of individual responses
|
137
|
+
# @return [Array<Hash>] The batch response array
|
138
|
+
def self.build_batch_response(responses)
|
139
|
+
# Filter out notification responses (id: nil)
|
140
|
+
responses.reject { |resp| resp[:id].nil? }
|
141
|
+
end
|
142
|
+
|
143
|
+
##
|
144
|
+
# Build an error response
|
145
|
+
#
|
146
|
+
# @param code [Integer] The error code
|
147
|
+
# @param message [String] The error message
|
148
|
+
# @param data [Object, nil] Additional error data
|
149
|
+
# @param id [String, Integer, nil] The request ID
|
150
|
+
# @return [Hash] The error response hash
|
151
|
+
def self.build_error_response(code:, message:, id:, data: nil)
|
152
|
+
error = { code: code, message: message }
|
153
|
+
error[:data] = data if data
|
154
|
+
|
155
|
+
build_response(error: error, id: id)
|
156
|
+
end
|
157
|
+
|
158
|
+
##
|
159
|
+
# Check if a hash represents a valid JSON-RPC 2.0 request
|
160
|
+
#
|
161
|
+
# @param hash [Hash] The hash to validate
|
162
|
+
# @return [Boolean] True if valid request format
|
163
|
+
def self.valid_request?(hash)
|
164
|
+
return false unless hash.is_a?(Hash)
|
165
|
+
return false unless hash["jsonrpc"] == JSONRPC_VERSION
|
166
|
+
return false unless hash["method"].is_a?(String)
|
167
|
+
|
168
|
+
# id can be string, number, or null (for notifications)
|
169
|
+
id = hash["id"]
|
170
|
+
return false unless id.nil? || id.is_a?(String) || id.is_a?(Integer)
|
171
|
+
|
172
|
+
# params is optional but must be object or array if present
|
173
|
+
params = hash["params"]
|
174
|
+
return false if params && !params.is_a?(Hash) && !params.is_a?(Array)
|
175
|
+
|
176
|
+
true
|
177
|
+
end
|
178
|
+
|
179
|
+
private_class_method def self.parse_single_request(hash)
|
180
|
+
raise A2A::Errors::InvalidRequest, "Invalid request format" unless valid_request?(hash)
|
181
|
+
|
182
|
+
Request.new(
|
183
|
+
jsonrpc: hash["jsonrpc"],
|
184
|
+
method: hash["method"],
|
185
|
+
params: hash["params"] || {},
|
186
|
+
id: hash["id"]
|
187
|
+
)
|
188
|
+
end
|
189
|
+
|
190
|
+
private_class_method def self.normalize_error(error)
|
191
|
+
if error.is_a?(A2A::Errors::A2AError)
|
192
|
+
error.to_json_rpc_error
|
193
|
+
elsif error.is_a?(Hash)
|
194
|
+
error
|
195
|
+
else
|
196
|
+
{ code: INTERNAL_ERROR, message: error.to_s }
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
##
|
202
|
+
# Represents a JSON-RPC 2.0 request
|
203
|
+
class Request
|
204
|
+
attr_reader :jsonrpc, :method, :params, :id
|
205
|
+
|
206
|
+
##
|
207
|
+
# Initialize a new request
|
208
|
+
#
|
209
|
+
# @param jsonrpc [String] The JSON-RPC version (should be "2.0")
|
210
|
+
# @param method [String] The method name
|
211
|
+
# @param params [Hash, Array] The method parameters
|
212
|
+
# @param id [String, Integer, nil] The request ID (nil for notifications)
|
213
|
+
def initialize(jsonrpc:, method:, params: {}, id: nil)
|
214
|
+
@jsonrpc = jsonrpc
|
215
|
+
@method = method
|
216
|
+
@params = params
|
217
|
+
@id = id
|
218
|
+
end
|
219
|
+
|
220
|
+
##
|
221
|
+
# Check if this is a notification (no response expected)
|
222
|
+
#
|
223
|
+
# @return [Boolean] True if this is a notification
|
224
|
+
def notification?
|
225
|
+
@id.nil?
|
226
|
+
end
|
227
|
+
|
228
|
+
##
|
229
|
+
# Convert to hash representation
|
230
|
+
#
|
231
|
+
# @return [Hash] The request as a hash
|
232
|
+
def to_h
|
233
|
+
hash = {
|
234
|
+
jsonrpc: @jsonrpc,
|
235
|
+
method: @method
|
236
|
+
}
|
237
|
+
|
238
|
+
hash[:params] = @params unless @params.nil? || (@params.respond_to?(:empty?) && @params.empty?)
|
239
|
+
hash[:id] = @id unless @id.nil?
|
240
|
+
|
241
|
+
hash
|
242
|
+
end
|
243
|
+
|
244
|
+
##
|
245
|
+
# Convert to JSON string
|
246
|
+
#
|
247
|
+
# @return [String] The request as JSON
|
248
|
+
def to_json(*_args)
|
249
|
+
# Use optimized JSON generator if available
|
250
|
+
JsonRpc.generate_json(to_h)
|
251
|
+
end
|
252
|
+
end
|
253
|
+
end
|
254
|
+
end
|
@@ -0,0 +1,258 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../utils/rails_detection"
|
4
|
+
|
5
|
+
##
|
6
|
+
# Main controller for A2A Rails engine
|
7
|
+
#
|
8
|
+
# This controller handles all A2A protocol endpoints including JSON-RPC requests,
|
9
|
+
# agent card serving, and streaming responses.
|
10
|
+
#
|
11
|
+
module A2A
|
12
|
+
module Rails
|
13
|
+
class A2aController < ApplicationController
|
14
|
+
include A2A::Rails::ControllerHelpers
|
15
|
+
include A2A::Utils::RailsDetection
|
16
|
+
|
17
|
+
# Skip CSRF protection for A2A endpoints
|
18
|
+
skip_before_action :verify_authenticity_token
|
19
|
+
|
20
|
+
# Handle JSON-RPC requests
|
21
|
+
def rpc
|
22
|
+
request_body = request.body.read
|
23
|
+
|
24
|
+
begin
|
25
|
+
json_rpc_request = A2A::Protocol::JsonRpc.parse_request(request_body)
|
26
|
+
|
27
|
+
# Handle batch requests
|
28
|
+
if json_rpc_request.is_a?(Array)
|
29
|
+
responses = json_rpc_request.map { |req| handle_single_request(req) }
|
30
|
+
render json: responses
|
31
|
+
else
|
32
|
+
response = handle_single_request(json_rpc_request)
|
33
|
+
render json: response
|
34
|
+
end
|
35
|
+
rescue A2A::Errors::A2AError => e
|
36
|
+
render json: build_error_response(e), status: :bad_request
|
37
|
+
rescue StandardError => e
|
38
|
+
error = A2A::Errors::InternalError.new(e.message)
|
39
|
+
render json: build_error_response(error), status: :internal_server_error
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# Serve agent card
|
44
|
+
def agent_card
|
45
|
+
card = generate_agent_card
|
46
|
+
|
47
|
+
# Support different output formats
|
48
|
+
case request.format.symbol
|
49
|
+
when :json
|
50
|
+
render json: card.to_h
|
51
|
+
when :jws
|
52
|
+
# TODO: Implement JWS signing
|
53
|
+
render json: { error: "JWS format not yet implemented" }, status: :not_implemented
|
54
|
+
else
|
55
|
+
render json: card.to_h
|
56
|
+
end
|
57
|
+
rescue StandardError => e
|
58
|
+
render json: { error: e.message }, status: :internal_server_error
|
59
|
+
end
|
60
|
+
|
61
|
+
# Serve authenticated agent card
|
62
|
+
def authenticated_agent_card
|
63
|
+
# Ensure authentication is present
|
64
|
+
unless authenticated?
|
65
|
+
render json: { error: "Authentication required" }, status: :unauthorized
|
66
|
+
return
|
67
|
+
end
|
68
|
+
|
69
|
+
card = generate_authenticated_agent_card
|
70
|
+
render json: card.to_h
|
71
|
+
rescue StandardError => e
|
72
|
+
render json: { error: e.message }, status: :internal_server_error
|
73
|
+
end
|
74
|
+
|
75
|
+
# List capabilities
|
76
|
+
def capabilities
|
77
|
+
capabilities = collect_capabilities
|
78
|
+
render json: { capabilities: capabilities }
|
79
|
+
rescue StandardError => e
|
80
|
+
render json: { error: e.message }, status: :internal_server_error
|
81
|
+
end
|
82
|
+
|
83
|
+
# Health check endpoint
|
84
|
+
def health
|
85
|
+
render json: {
|
86
|
+
status: "healthy",
|
87
|
+
version: A2A::VERSION,
|
88
|
+
timestamp: Time.now.iso8601,
|
89
|
+
rails_version: rails_version || "unknown"
|
90
|
+
}
|
91
|
+
end
|
92
|
+
|
93
|
+
# Server-Sent Events streaming endpoint
|
94
|
+
def stream
|
95
|
+
task_id = params[:task_id]
|
96
|
+
|
97
|
+
begin
|
98
|
+
task = A2A::Server::TaskManager.instance.get_task(task_id)
|
99
|
+
|
100
|
+
response.headers["Content-Type"] = "text/event-stream"
|
101
|
+
response.headers["Cache-Control"] = "no-cache"
|
102
|
+
response.headers["Connection"] = "keep-alive"
|
103
|
+
|
104
|
+
# Set up SSE stream
|
105
|
+
sse = A2A::Transport::SSE.new(response.stream)
|
106
|
+
|
107
|
+
# Send initial task status
|
108
|
+
sse.write_event("task-status", task.to_h)
|
109
|
+
|
110
|
+
# Subscribe to task updates
|
111
|
+
subscription = A2A::Server::TaskManager.instance.subscribe_to_task(task_id) do |event|
|
112
|
+
case event
|
113
|
+
when A2A::Types::TaskStatusUpdateEvent
|
114
|
+
sse.write_event("task-status-update", event.to_h)
|
115
|
+
when A2A::Types::TaskArtifactUpdateEvent
|
116
|
+
sse.write_event("task-artifact-update", event.to_h)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
# Keep connection alive until client disconnects
|
121
|
+
loop do
|
122
|
+
break if response.stream.closed?
|
123
|
+
|
124
|
+
sleep 1
|
125
|
+
sse.write_event("heartbeat", { timestamp: Time.now.iso8601 })
|
126
|
+
end
|
127
|
+
rescue A2A::Errors::TaskNotFound
|
128
|
+
render json: { error: "Task not found" }, status: :not_found
|
129
|
+
rescue StandardError => e
|
130
|
+
render json: { error: e.message }, status: :internal_server_error
|
131
|
+
ensure
|
132
|
+
subscription&.unsubscribe
|
133
|
+
response.stream.close
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
# Webhook endpoint for push notifications
|
138
|
+
def webhook
|
139
|
+
task_id = params[:task_id]
|
140
|
+
|
141
|
+
begin
|
142
|
+
# Verify webhook authentication if configured
|
143
|
+
verify_webhook_authentication! if webhook_authentication_required?
|
144
|
+
|
145
|
+
# Process webhook payload
|
146
|
+
payload = JSON.parse(request.body.read)
|
147
|
+
|
148
|
+
# Handle different webhook types
|
149
|
+
case payload["type"]
|
150
|
+
when "task_status_update"
|
151
|
+
handle_task_status_webhook(task_id, payload)
|
152
|
+
when "task_artifact_update"
|
153
|
+
handle_task_artifact_webhook(task_id, payload)
|
154
|
+
else
|
155
|
+
render json: { error: "Unknown webhook type" }, status: :bad_request
|
156
|
+
return
|
157
|
+
end
|
158
|
+
|
159
|
+
render json: { status: "processed" }
|
160
|
+
rescue JSON::ParserError
|
161
|
+
render json: { error: "Invalid JSON payload" }, status: :bad_request
|
162
|
+
rescue A2A::Errors::TaskNotFound
|
163
|
+
render json: { error: "Task not found" }, status: :not_found
|
164
|
+
rescue A2A::Errors::AuthenticationError
|
165
|
+
render json: { error: "Webhook authentication failed" }, status: :unauthorized
|
166
|
+
rescue StandardError => e
|
167
|
+
render json: { error: e.message }, status: :internal_server_error
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
private
|
172
|
+
|
173
|
+
def handle_single_request(json_rpc_request)
|
174
|
+
# Delegate to the A2A request handler
|
175
|
+
handle_a2a_request(json_rpc_request)
|
176
|
+
rescue A2A::Errors::A2AError => e
|
177
|
+
build_error_response(e, json_rpc_request.id)
|
178
|
+
rescue StandardError => e
|
179
|
+
error = A2A::Errors::InternalError.new(e.message)
|
180
|
+
build_error_response(error, json_rpc_request.id)
|
181
|
+
end
|
182
|
+
|
183
|
+
def build_error_response(error, id = nil)
|
184
|
+
A2A::Protocol::JsonRpc.build_response(
|
185
|
+
error: error.to_json_rpc_error,
|
186
|
+
id: id
|
187
|
+
)
|
188
|
+
end
|
189
|
+
|
190
|
+
def generate_authenticated_agent_card
|
191
|
+
# Generate extended agent card with authentication context
|
192
|
+
card = generate_agent_card
|
193
|
+
|
194
|
+
# Add authenticated-specific information
|
195
|
+
card_hash = card.to_h
|
196
|
+
card_hash[:authenticated_user] = current_user_info if respond_to?(:current_user_info)
|
197
|
+
card_hash[:permissions] = current_user_permissions if respond_to?(:current_user_permissions)
|
198
|
+
|
199
|
+
A2A::Types::AgentCard.from_h(card_hash)
|
200
|
+
end
|
201
|
+
|
202
|
+
def collect_capabilities
|
203
|
+
# Collect all registered A2A capabilities from controllers
|
204
|
+
capabilities = []
|
205
|
+
|
206
|
+
# Scan all controllers that include A2A::Server::Agent
|
207
|
+
ObjectSpace.each_object(Class) do |klass|
|
208
|
+
if klass < ActionController::Base && klass.included_modules.include?(A2A::Server::Agent)
|
209
|
+
capabilities.concat(klass._a2a_capabilities || [])
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
capabilities.map(&:to_h)
|
214
|
+
end
|
215
|
+
|
216
|
+
def authenticated?
|
217
|
+
# Check if request is authenticated
|
218
|
+
# This can be overridden by applications to integrate with their auth system
|
219
|
+
request.headers["Authorization"].present? ||
|
220
|
+
(respond_to?(:current_user) && current_user.present?)
|
221
|
+
end
|
222
|
+
|
223
|
+
def webhook_authentication_required?
|
224
|
+
A2A.config.webhook_authentication_required || false
|
225
|
+
end
|
226
|
+
|
227
|
+
def verify_webhook_authentication!
|
228
|
+
# Verify webhook signature or token
|
229
|
+
# This should be implemented based on the specific authentication method
|
230
|
+
auth_header = request.headers["Authorization"]
|
231
|
+
|
232
|
+
return if auth_header.present?
|
233
|
+
|
234
|
+
raise A2A::Errors::AuthenticationError, "Missing webhook authentication"
|
235
|
+
|
236
|
+
# TODO: Implement specific webhook authentication logic
|
237
|
+
# This could verify HMAC signatures, JWT tokens, etc.
|
238
|
+
end
|
239
|
+
|
240
|
+
def handle_task_status_webhook(task_id, payload)
|
241
|
+
# Process task status update webhook
|
242
|
+
status_data = payload["status"]
|
243
|
+
status = A2A::Types::TaskStatus.from_h(status_data)
|
244
|
+
|
245
|
+
A2A::Server::TaskManager.instance.update_task_status(task_id, status)
|
246
|
+
end
|
247
|
+
|
248
|
+
def handle_task_artifact_webhook(task_id, payload)
|
249
|
+
# Process task artifact update webhook
|
250
|
+
artifact_data = payload["artifact"]
|
251
|
+
artifact = A2A::Types::Artifact.from_h(artifact_data)
|
252
|
+
append = payload["append"] || false
|
253
|
+
|
254
|
+
A2A::Server::TaskManager.instance.update_task_artifact(task_id, artifact, append: append)
|
255
|
+
end
|
256
|
+
end
|
257
|
+
end
|
258
|
+
end
|