a2a-rb 0.1.1
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/CHANGELOG.md +38 -0
- data/LICENSE.txt +21 -0
- data/README.md +311 -0
- data/lib/a2a/agent_capabilities.rb +32 -0
- data/lib/a2a/agent_card/builder.rb +135 -0
- data/lib/a2a/agent_card/signature.rb +35 -0
- data/lib/a2a/agent_card/verifier.rb +19 -0
- data/lib/a2a/agent_card.rb +118 -0
- data/lib/a2a/agent_extension.rb +32 -0
- data/lib/a2a/agent_interface.rb +54 -0
- data/lib/a2a/agent_provider.rb +20 -0
- data/lib/a2a/agent_skill.rb +46 -0
- data/lib/a2a/artifact.rb +40 -0
- data/lib/a2a/client.rb +109 -0
- data/lib/a2a/discovery.rb +49 -0
- data/lib/a2a/json_rpc_envelope.rb +32 -0
- data/lib/a2a/message.rb +54 -0
- data/lib/a2a/oauth_flow/authorization_code.rb +37 -0
- data/lib/a2a/oauth_flow/client_credentials.rb +31 -0
- data/lib/a2a/oauth_flow/device_code.rb +34 -0
- data/lib/a2a/oauth_flow.rb +37 -0
- data/lib/a2a/operation/cancel_task.rb +39 -0
- data/lib/a2a/operation/create_task_push_notification_config.rb +38 -0
- data/lib/a2a/operation/delete_task_push_notification_config.rb +38 -0
- data/lib/a2a/operation/executable.rb +27 -0
- data/lib/a2a/operation/get_extended_agent_card.rb +32 -0
- data/lib/a2a/operation/get_task.rb +39 -0
- data/lib/a2a/operation/get_task_push_notification_config.rb +38 -0
- data/lib/a2a/operation/list_task_push_notification_configs.rb +64 -0
- data/lib/a2a/operation/list_tasks.rb +78 -0
- data/lib/a2a/operation/send_message/configuration.rb +39 -0
- data/lib/a2a/operation/send_message.rb +58 -0
- data/lib/a2a/operation/send_message_request.rb +37 -0
- data/lib/a2a/operation/send_streaming_message.rb +53 -0
- data/lib/a2a/operation/subscribe_to_task.rb +40 -0
- data/lib/a2a/operation.rb +19 -0
- data/lib/a2a/part/data.rb +34 -0
- data/lib/a2a/part/file.rb +45 -0
- data/lib/a2a/part/text.rb +34 -0
- data/lib/a2a/part.rb +21 -0
- data/lib/a2a/protocol/http_json/transport.rb +82 -0
- data/lib/a2a/protocol/http_json.rb +53 -0
- data/lib/a2a/protocol/json_rpc/transport.rb +54 -0
- data/lib/a2a/protocol/json_rpc.rb +55 -0
- data/lib/a2a/push_notification/authentication_info.rb +29 -0
- data/lib/a2a/push_notification/config.rb +40 -0
- data/lib/a2a/push_notification/dispatcher.rb +52 -0
- data/lib/a2a/push_notification/receiver.rb +54 -0
- data/lib/a2a/push_notification.rb +11 -0
- data/lib/a2a/role.rb +13 -0
- data/lib/a2a/security_requirement.rb +19 -0
- data/lib/a2a/security_scheme/api_key.rb +33 -0
- data/lib/a2a/security_scheme/http_auth.rb +33 -0
- data/lib/a2a/security_scheme/mutual_tls.rb +25 -0
- data/lib/a2a/security_scheme/oauth2.rb +52 -0
- data/lib/a2a/security_scheme/open_id_connect.rb +30 -0
- data/lib/a2a/security_scheme.rb +26 -0
- data/lib/a2a/streaming/artifact_update_event.rb +40 -0
- data/lib/a2a/streaming/response.rb +65 -0
- data/lib/a2a/streaming/sse_parser.rb +43 -0
- data/lib/a2a/streaming/sse_writer.rb +25 -0
- data/lib/a2a/streaming/status_update_event.rb +43 -0
- data/lib/a2a/streaming/subscription.rb +56 -0
- data/lib/a2a/streaming.rb +12 -0
- data/lib/a2a/task/state.rb +31 -0
- data/lib/a2a/task/status.rb +35 -0
- data/lib/a2a/task.rb +66 -0
- data/lib/a2a/version.rb +6 -0
- data/lib/a2a/versioning.rb +28 -0
- data/lib/a2a.rb +90 -0
- metadata +116 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module A2A
|
|
4
|
+
class Part
|
|
5
|
+
class Data
|
|
6
|
+
attr_reader :data, :media_type, :filename, :metadata
|
|
7
|
+
|
|
8
|
+
def initialize(data:, media_type: nil, filename: nil, metadata: nil)
|
|
9
|
+
@data = data
|
|
10
|
+
@media_type = media_type
|
|
11
|
+
@filename = filename
|
|
12
|
+
@metadata = metadata
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.from_h(hash)
|
|
16
|
+
new(
|
|
17
|
+
data: hash.fetch("data"),
|
|
18
|
+
media_type: hash["mediaType"],
|
|
19
|
+
filename: hash["filename"],
|
|
20
|
+
metadata: hash["metadata"]
|
|
21
|
+
)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def to_h
|
|
25
|
+
{
|
|
26
|
+
"data" => data,
|
|
27
|
+
"mediaType" => media_type,
|
|
28
|
+
"filename" => filename,
|
|
29
|
+
"metadata" => metadata
|
|
30
|
+
}.compact
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module A2A
|
|
4
|
+
class Part
|
|
5
|
+
class File
|
|
6
|
+
attr_reader :raw, :url, :filename, :media_type, :metadata
|
|
7
|
+
|
|
8
|
+
def initialize(raw: nil, url: nil, filename: nil, media_type: nil, metadata: nil)
|
|
9
|
+
@raw = raw
|
|
10
|
+
@url = url
|
|
11
|
+
@filename = filename
|
|
12
|
+
@media_type = media_type
|
|
13
|
+
@metadata = metadata
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.from_h(hash)
|
|
17
|
+
new(
|
|
18
|
+
raw: hash["raw"],
|
|
19
|
+
url: hash["url"],
|
|
20
|
+
filename: hash["filename"],
|
|
21
|
+
media_type: hash["mediaType"],
|
|
22
|
+
metadata: hash["metadata"]
|
|
23
|
+
)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def to_h
|
|
27
|
+
{
|
|
28
|
+
"raw" => raw,
|
|
29
|
+
"url" => url,
|
|
30
|
+
"filename" => filename,
|
|
31
|
+
"mediaType" => media_type,
|
|
32
|
+
"metadata" => metadata
|
|
33
|
+
}.compact
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def inline?
|
|
37
|
+
!raw.nil?
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def remote?
|
|
41
|
+
!url.nil?
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module A2A
|
|
4
|
+
class Part
|
|
5
|
+
class Text
|
|
6
|
+
attr_reader :text, :media_type, :filename, :metadata
|
|
7
|
+
|
|
8
|
+
def initialize(text:, media_type: nil, filename: nil, metadata: nil)
|
|
9
|
+
@text = text
|
|
10
|
+
@media_type = media_type
|
|
11
|
+
@filename = filename
|
|
12
|
+
@metadata = metadata
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.from_h(hash)
|
|
16
|
+
new(
|
|
17
|
+
text: hash.fetch("text"),
|
|
18
|
+
media_type: hash["mediaType"],
|
|
19
|
+
filename: hash["filename"],
|
|
20
|
+
metadata: hash["metadata"]
|
|
21
|
+
)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def to_h
|
|
25
|
+
{
|
|
26
|
+
"text" => text,
|
|
27
|
+
"mediaType" => media_type,
|
|
28
|
+
"filename" => filename,
|
|
29
|
+
"metadata" => metadata
|
|
30
|
+
}.compact
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
data/lib/a2a/part.rb
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "part/text"
|
|
4
|
+
require_relative "part/data"
|
|
5
|
+
require_relative "part/file"
|
|
6
|
+
|
|
7
|
+
module A2A
|
|
8
|
+
class Part
|
|
9
|
+
def self.from_h(hash)
|
|
10
|
+
if hash.key?("text")
|
|
11
|
+
Part::Text.from_h(hash)
|
|
12
|
+
elsif hash.key?("data")
|
|
13
|
+
Part::Data.from_h(hash)
|
|
14
|
+
elsif hash.key?("raw") || hash.key?("url")
|
|
15
|
+
Part::File.from_h(hash)
|
|
16
|
+
else
|
|
17
|
+
raise ArgumentError, "cannot detect Part type from keys: #{hash.keys.inspect}"
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "json"
|
|
5
|
+
require "uri"
|
|
6
|
+
|
|
7
|
+
module A2A
|
|
8
|
+
module Protocol
|
|
9
|
+
class HttpJson
|
|
10
|
+
class Transport
|
|
11
|
+
def get(url, query:, headers:)
|
|
12
|
+
uri = URI.parse(url)
|
|
13
|
+
uri.query = URI.encode_www_form(query) unless query.empty?
|
|
14
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
15
|
+
http.use_ssl = uri.scheme == "https"
|
|
16
|
+
request = Net::HTTP::Get.new(uri.request_uri, headers)
|
|
17
|
+
response = http.request(request)
|
|
18
|
+
handle_response(response)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def post(url, body:, headers:)
|
|
22
|
+
uri = URI.parse(url)
|
|
23
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
24
|
+
http.use_ssl = uri.scheme == "https"
|
|
25
|
+
request = Net::HTTP::Post.new(uri.request_uri, headers)
|
|
26
|
+
request.body = JSON.generate(body)
|
|
27
|
+
response = http.request(request)
|
|
28
|
+
handle_response(response)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def delete(url, headers:)
|
|
32
|
+
uri = URI.parse(url)
|
|
33
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
34
|
+
http.use_ssl = uri.scheme == "https"
|
|
35
|
+
response = http.request(Net::HTTP::Delete.new(uri.request_uri, headers))
|
|
36
|
+
handle_delete_response(response)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def stream(url, headers:, method: :post, body: {}, query: {}, &block)
|
|
40
|
+
uri = URI.parse(url)
|
|
41
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
42
|
+
http.use_ssl = uri.scheme == "https"
|
|
43
|
+
http.request(build_stream_request(uri, method, headers, body, query), &block)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def build_stream_request(uri, method, headers, body, query)
|
|
49
|
+
case method
|
|
50
|
+
when :post
|
|
51
|
+
req = Net::HTTP::Post.new(uri.request_uri, headers)
|
|
52
|
+
req.body = JSON.generate(body)
|
|
53
|
+
req
|
|
54
|
+
when :get
|
|
55
|
+
uri.query = URI.encode_www_form(query) unless query.empty?
|
|
56
|
+
Net::HTTP::Get.new(uri.request_uri, headers)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def handle_response(response)
|
|
61
|
+
case response.code.to_i
|
|
62
|
+
when 200..299 then JSON.parse(response.body)
|
|
63
|
+
when 401 then raise AuthenticationError, "HTTP 401"
|
|
64
|
+
when 403 then raise AuthorizationError, "HTTP 403"
|
|
65
|
+
when 404 then raise TaskNotFoundError, "HTTP 404"
|
|
66
|
+
else raise TransportError, "HTTP #{response.code}"
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def handle_delete_response(response)
|
|
71
|
+
case response.code.to_i
|
|
72
|
+
when 200..299 then nil
|
|
73
|
+
when 401 then raise AuthenticationError, "HTTP 401"
|
|
74
|
+
when 403 then raise AuthorizationError, "HTTP 403"
|
|
75
|
+
when 404 then raise TaskNotFoundError, "HTTP 404"
|
|
76
|
+
else raise TransportError, "HTTP #{response.code}"
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "http_json/transport"
|
|
4
|
+
|
|
5
|
+
module A2A
|
|
6
|
+
module Protocol
|
|
7
|
+
class HttpJson
|
|
8
|
+
attr_reader :url, :version, :headers, :extensions
|
|
9
|
+
|
|
10
|
+
def initialize(url:, version: Versioning::CURRENT, headers: {}, extensions: [], transport: Transport.new)
|
|
11
|
+
@url = url.chomp("/")
|
|
12
|
+
@version = version
|
|
13
|
+
@headers = headers
|
|
14
|
+
@extensions = extensions
|
|
15
|
+
@transport = transport
|
|
16
|
+
@built_headers = build_headers
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def get(path, query: {})
|
|
20
|
+
@transport.get("#{@url}#{path}", query: query, headers: @built_headers)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def post(path, body: {})
|
|
24
|
+
@transport.post("#{@url}#{path}", body: body, headers: @built_headers)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def delete(path)
|
|
28
|
+
@transport.delete("#{@url}#{path}", headers: @built_headers)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def stream(path, method: :post, body: {}, query: {}, &)
|
|
32
|
+
sse_headers = @built_headers.merge("Accept" => "text/event-stream")
|
|
33
|
+
@transport.stream("#{@url}#{path}", headers: sse_headers, method: method, body: body, query: query, &)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def build_headers
|
|
39
|
+
h = default_headers.merge(@headers)
|
|
40
|
+
h["A2A-Extensions"] = @extensions.join(", ") unless @extensions.empty?
|
|
41
|
+
h
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def default_headers
|
|
45
|
+
{
|
|
46
|
+
"Content-Type" => "application/json",
|
|
47
|
+
"Accept" => "application/json",
|
|
48
|
+
"A2A-Version" => @version
|
|
49
|
+
}
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "json"
|
|
5
|
+
require "uri"
|
|
6
|
+
|
|
7
|
+
module A2A
|
|
8
|
+
module Protocol
|
|
9
|
+
class JsonRpc
|
|
10
|
+
class Transport
|
|
11
|
+
def post(url, body:, headers:)
|
|
12
|
+
uri = URI.parse(url)
|
|
13
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
14
|
+
http.use_ssl = uri.scheme == "https"
|
|
15
|
+
request = Net::HTTP::Post.new(uri.request_uri, headers)
|
|
16
|
+
request.body = JSON.generate(body)
|
|
17
|
+
response = http.request(request)
|
|
18
|
+
handle_response(response)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def stream(url, headers:, method: :post, body: {}, query: {}, &block)
|
|
22
|
+
uri = URI.parse(url)
|
|
23
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
24
|
+
http.use_ssl = uri.scheme == "https"
|
|
25
|
+
http.request(build_stream_request(uri, method, headers, body, query), &block)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def build_stream_request(uri, method, headers, body, query)
|
|
31
|
+
case method
|
|
32
|
+
when :post
|
|
33
|
+
req = Net::HTTP::Post.new(uri.request_uri, headers)
|
|
34
|
+
req.body = JSON.generate(body)
|
|
35
|
+
req
|
|
36
|
+
when :get
|
|
37
|
+
uri.query = URI.encode_www_form(query) unless query.empty?
|
|
38
|
+
Net::HTTP::Get.new(uri.request_uri, headers)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def handle_response(response)
|
|
43
|
+
case response.code.to_i
|
|
44
|
+
when 200..299 then JSON.parse(response.body)
|
|
45
|
+
when 401 then raise AuthenticationError, "HTTP 401"
|
|
46
|
+
when 403 then raise AuthorizationError, "HTTP 403"
|
|
47
|
+
when 404 then raise TaskNotFoundError, "HTTP 404"
|
|
48
|
+
else raise TransportError, "HTTP #{response.code}"
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
require_relative "json_rpc/transport"
|
|
5
|
+
|
|
6
|
+
module A2A
|
|
7
|
+
module Protocol
|
|
8
|
+
class JsonRpc
|
|
9
|
+
attr_reader :url, :version, :headers, :extensions
|
|
10
|
+
|
|
11
|
+
def initialize(url:, version: Versioning::CURRENT, headers: {}, extensions: [], transport: Transport.new)
|
|
12
|
+
@url = url
|
|
13
|
+
@version = version
|
|
14
|
+
@headers = headers
|
|
15
|
+
@extensions = extensions
|
|
16
|
+
@transport = transport
|
|
17
|
+
@built_headers = build_headers
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def post(method, params = {})
|
|
21
|
+
@transport.post(@url, body: build_envelope(method, params), headers: @built_headers)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def stream(method, params = {}, &)
|
|
25
|
+
sse_headers = @built_headers.merge("Accept" => "text/event-stream")
|
|
26
|
+
@transport.stream(@url, headers: sse_headers, method: :post, body: build_envelope(method, params), &)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def build_envelope(method, params)
|
|
32
|
+
{
|
|
33
|
+
"jsonrpc" => "2.0",
|
|
34
|
+
"method" => method,
|
|
35
|
+
"id" => SecureRandom.uuid,
|
|
36
|
+
"params" => params
|
|
37
|
+
}
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def build_headers
|
|
41
|
+
h = default_headers.merge(@headers)
|
|
42
|
+
h["A2A-Extensions"] = @extensions.join(", ") unless @extensions.empty?
|
|
43
|
+
h
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def default_headers
|
|
47
|
+
{
|
|
48
|
+
"Content-Type" => "application/json",
|
|
49
|
+
"Accept" => "application/json",
|
|
50
|
+
"A2A-Version" => @version
|
|
51
|
+
}
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module A2A
|
|
4
|
+
module PushNotification
|
|
5
|
+
class AuthenticationInfo
|
|
6
|
+
attr_reader :scheme, :credentials
|
|
7
|
+
|
|
8
|
+
def initialize(scheme:, credentials: nil)
|
|
9
|
+
@scheme = scheme
|
|
10
|
+
@credentials = credentials
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.from_h(hash)
|
|
14
|
+
new(scheme: hash.fetch("scheme"), credentials: hash["credentials"])
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def to_h
|
|
18
|
+
{
|
|
19
|
+
"scheme" => scheme,
|
|
20
|
+
"credentials" => credentials
|
|
21
|
+
}.compact
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def authorization_header
|
|
25
|
+
credentials ? "#{scheme} #{credentials}" : scheme
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module A2A
|
|
4
|
+
module PushNotification
|
|
5
|
+
class Config
|
|
6
|
+
attr_reader :id, :url, :token, :tenant, :task_id, :authentication
|
|
7
|
+
|
|
8
|
+
def initialize(url:, **kwargs)
|
|
9
|
+
@url = url
|
|
10
|
+
@id = kwargs[:id]
|
|
11
|
+
@token = kwargs[:token]
|
|
12
|
+
@tenant = kwargs[:tenant]
|
|
13
|
+
@task_id = kwargs[:task_id]
|
|
14
|
+
@authentication = kwargs[:authentication]
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def self.from_h(hash)
|
|
18
|
+
new(
|
|
19
|
+
url: hash.fetch("url"),
|
|
20
|
+
id: hash["id"],
|
|
21
|
+
token: hash["token"],
|
|
22
|
+
tenant: hash["tenant"],
|
|
23
|
+
task_id: hash["taskId"],
|
|
24
|
+
authentication: hash["authentication"] && AuthenticationInfo.from_h(hash["authentication"])
|
|
25
|
+
)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def to_h
|
|
29
|
+
{
|
|
30
|
+
"url" => url,
|
|
31
|
+
"id" => id,
|
|
32
|
+
"token" => token,
|
|
33
|
+
"tenant" => tenant,
|
|
34
|
+
"taskId" => task_id,
|
|
35
|
+
"authentication" => authentication
|
|
36
|
+
}.compact
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "json"
|
|
5
|
+
require "uri"
|
|
6
|
+
|
|
7
|
+
module A2A
|
|
8
|
+
module PushNotification
|
|
9
|
+
class Dispatcher
|
|
10
|
+
def initialize(transport: Transport.new)
|
|
11
|
+
@transport = transport
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def dispatch(config, event)
|
|
15
|
+
headers = {
|
|
16
|
+
"Content-Type" => "application/json",
|
|
17
|
+
"Accept" => "application/json"
|
|
18
|
+
}
|
|
19
|
+
headers["Authorization"] = config.authentication.authorization_header if config.authentication
|
|
20
|
+
|
|
21
|
+
@transport.post(config.url, body: event.to_h, headers: headers)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
class Transport
|
|
25
|
+
def post(url, body:, headers:)
|
|
26
|
+
uri = URI.parse(url)
|
|
27
|
+
handle(http_for(uri).request(build_request(uri, body, headers)))
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def handle(response)
|
|
33
|
+
return if (200..299).cover?(response.code.to_i)
|
|
34
|
+
|
|
35
|
+
raise TransportError, "push notification delivery failed: HTTP #{response.code}"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def http_for(uri)
|
|
39
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
40
|
+
http.use_ssl = uri.scheme == "https"
|
|
41
|
+
http
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def build_request(uri, body, headers)
|
|
45
|
+
request = Net::HTTP::Post.new(uri.request_uri, headers)
|
|
46
|
+
request.body = JSON.generate(body)
|
|
47
|
+
request
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rack"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
module A2A
|
|
7
|
+
module PushNotification
|
|
8
|
+
class Receiver
|
|
9
|
+
def initialize(app, path: "/a2a/webhook", scheme: "Bearer", credentials: nil, &handler)
|
|
10
|
+
@app = app
|
|
11
|
+
@path = path
|
|
12
|
+
@scheme = scheme
|
|
13
|
+
@credentials = credentials
|
|
14
|
+
@handler = handler
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def call(env)
|
|
18
|
+
request = Rack::Request.new(env)
|
|
19
|
+
return @app.call(env) unless request.post? && request.path == @path
|
|
20
|
+
return unauthorized unless authorized?(request)
|
|
21
|
+
|
|
22
|
+
handle_event(request)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def handle_event(request)
|
|
28
|
+
event = Streaming::Response.from_h(JSON.parse(request.body.read))
|
|
29
|
+
@handler.call(event)
|
|
30
|
+
json(200, { "ok" => true })
|
|
31
|
+
rescue JSON::ParserError => e
|
|
32
|
+
json(400, { "error" => "invalid JSON: #{e.message}" })
|
|
33
|
+
rescue ArgumentError => e
|
|
34
|
+
json(400, { "error" => e.message })
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def authorized?(request)
|
|
38
|
+
return true unless @credentials
|
|
39
|
+
|
|
40
|
+
expected = "#{@scheme} #{@credentials}"
|
|
41
|
+
actual = request.get_header("HTTP_AUTHORIZATION").to_s
|
|
42
|
+
actual.casecmp(expected).zero?
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def unauthorized
|
|
46
|
+
json(401, { "error" => "unauthorized" })
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def json(status, body)
|
|
50
|
+
[status, { "Content-Type" => "application/json" }, [JSON.generate(body)]]
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "push_notification/authentication_info"
|
|
4
|
+
require_relative "push_notification/config"
|
|
5
|
+
require_relative "push_notification/dispatcher"
|
|
6
|
+
require_relative "push_notification/receiver"
|
|
7
|
+
|
|
8
|
+
module A2A
|
|
9
|
+
module PushNotification
|
|
10
|
+
end
|
|
11
|
+
end
|
data/lib/a2a/role.rb
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module A2A
|
|
4
|
+
class SecurityRequirement
|
|
5
|
+
attr_reader :schemes
|
|
6
|
+
|
|
7
|
+
def initialize(schemes:)
|
|
8
|
+
@schemes = schemes
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def self.from_h(hash)
|
|
12
|
+
new(schemes: hash.transform_values { _1.is_a?(Array) ? _1 : _1["list"] })
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def to_h
|
|
16
|
+
schemes
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module A2A
|
|
4
|
+
module SecurityScheme
|
|
5
|
+
class APIKey
|
|
6
|
+
attr_reader :name, :location, :description
|
|
7
|
+
|
|
8
|
+
def initialize(name:, location:, description: nil)
|
|
9
|
+
@name = name
|
|
10
|
+
@location = location
|
|
11
|
+
@description = description
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.from_h(hash)
|
|
15
|
+
new(
|
|
16
|
+
name: hash.fetch("name"),
|
|
17
|
+
location: hash.fetch("location"),
|
|
18
|
+
description: hash["description"]
|
|
19
|
+
)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def to_h
|
|
23
|
+
{
|
|
24
|
+
"apiKeySecurityScheme" => {
|
|
25
|
+
"name" => name,
|
|
26
|
+
"location" => location,
|
|
27
|
+
"description" => description
|
|
28
|
+
}.compact
|
|
29
|
+
}
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module A2A
|
|
4
|
+
module SecurityScheme
|
|
5
|
+
class HTTPAuth
|
|
6
|
+
attr_reader :scheme, :bearer_format, :description
|
|
7
|
+
|
|
8
|
+
def initialize(scheme:, bearer_format: nil, description: nil)
|
|
9
|
+
@scheme = scheme
|
|
10
|
+
@bearer_format = bearer_format
|
|
11
|
+
@description = description
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.from_h(hash)
|
|
15
|
+
new(
|
|
16
|
+
scheme: hash.fetch("scheme"),
|
|
17
|
+
bearer_format: hash["bearerFormat"],
|
|
18
|
+
description: hash["description"]
|
|
19
|
+
)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def to_h
|
|
23
|
+
{
|
|
24
|
+
"httpAuthSecurityScheme" => {
|
|
25
|
+
"scheme" => scheme,
|
|
26
|
+
"bearerFormat" => bearer_format,
|
|
27
|
+
"description" => description
|
|
28
|
+
}.compact
|
|
29
|
+
}
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|