keycardai-a2a 0.1.0 → 0.2.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 +4 -4
- data/CHANGELOG.md +28 -0
- data/README.md +14 -4
- data/lib/keycardai/a2a/delegation_client.rb +23 -14
- data/lib/keycardai/a2a/version.rb +1 -1
- data/lib/keycardai/a2a/wire.rb +99 -0
- data/lib/keycardai/a2a.rb +3 -5
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b1573597b5a3ee265b3a30b56b8e0681b70b5993aefd619da4f761efcd46eac1
|
|
4
|
+
data.tar.gz: a42a9591d4fdeaf4a409aad1b7b9a2f83ccf064f17ae8e739105d1ed70e318ea
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fc0107c8c5b975c24518c9589e64d0d9afe0db248c1dc49a121fcd6deb3ad6dfd87150376eaab736a3bcbe091fbefad6e149fae8b3813e452af7ab44358eac64
|
|
7
|
+
data.tar.gz: 8281680b4456cc7066e404875ea766282b4d28adb2e1c2221933997748e73ffa950f2ed73ef4b7267ea75d09811577ef0d1ee1842b21f1694b668d851247c08e
|
data/CHANGELOG.md
CHANGED
|
@@ -12,3 +12,31 @@ Agent-to-agent delegation: agent card discovery with caching, per-hop RFC 8693
|
|
|
12
12
|
token exchange that keeps the user as the subject, and JSON-RPC `message/send`
|
|
13
13
|
invocation. Wraps no A2A SDK; hosting an agent inside a framework is out of
|
|
14
14
|
scope.
|
|
15
|
+
|
|
16
|
+
## 0.2.0-keycardai-a2a (2026-09-07)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
- feat(keycardai-a2a): speak A2A protocol 1.0 by default
|
|
20
|
+
- ECO-161 Ruby leg, after typescript-sdk #173 and go-sdk #47. DelegationClient sends 1.0-generation requests by default (SendMessage, A2A-Version: 1.0, ROLE_USER roles, untagged text parts, endpoint read from the card's JSONRPC supportedInterfaces entry) instead of the 0.3 envelope, so invoking a keycardai-a2a (Python) or @keycardai/a2a agent no longer returns MethodNotFound. protocol_version: now selects the whole wire generation through Keycardai::A2A::Wire: LEGACY_PROTOCOL_VERSION sends a real 0.3 envelope (role and part shapes translated on the way out), and any other value raises ArgumentError instead of being sent verbatim. A2A.text_message builds the 1.0 shape either way. The JSON-RPC result is returned as the agent sent it, the gem's existing contract.
|
|
21
|
+
- BREAKING CHANGE(keycardai-a2a): PROTOCOL_VERSION and MESSAGE_SEND_METHOD constant values moved to the 1.0 generation, and 0.3 interop requires protocol_version: LEGACY_PROTOCOL_VERSION.
|
|
22
|
+
|
|
23
|
+
## 0.1.0-keycardai-a2a (2026-08-19)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
- feat(keycardai-a2a): delegation client and agent card discovery (#10)
|
|
27
|
+
- Phase 3: the A2A delegation contract per specs/a2a/a2a-delegation.md,
|
|
28
|
+
matching the Go boundary (delegation client only; agent-framework
|
|
29
|
+
hosting glue is out of scope, and no community A2A gem dependency):
|
|
30
|
+
- - ServiceDiscovery: agent card fetch from /.well-known/agent-card.json
|
|
31
|
+
with a 15-minute refreshable cache; a card must carry a name
|
|
32
|
+
- DelegationClient: discover, RFC 8693 exchange (subject = the user
|
|
33
|
+
token, resource = the target agent, this agent authenticates with its
|
|
34
|
+
credential, no actor_token), then JSON-RPC message/send with the
|
|
35
|
+
exchanged bearer token and X-A2A-Protocol-Version 0.3; the invocation
|
|
36
|
+
endpoint is read from the card or derived by convention
|
|
37
|
+
- Typed DiscoveryError / InvocationError (with the JSON-RPC error
|
|
38
|
+
payload); exchange failures surface as the oauth OAuthError per hop
|
|
39
|
+
- A2A.text_message convenience for single-text-part params
|
|
40
|
+
- Conformance suite maps to all five spec unit rows, plus card-declared
|
|
41
|
+
endpoint and cache-TTL coverage; the multi-hop act-chain rows are the
|
|
42
|
+
integration table for the E2E phase.
|
data/README.md
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
Agent-to-agent delegation for Ruby agents on the Keycard platform.
|
|
4
4
|
|
|
5
|
-
> **Preview.** APIs may change between minor versions while the surface settles.
|
|
6
5
|
> Conformance against the cross-SDK contract is tracked in the
|
|
7
6
|
> [conformance report](https://github.com/keycardai/ruby-sdk/blob/main/docs/conformance-report.md).
|
|
8
7
|
|
|
@@ -19,8 +18,8 @@ Implements the delegation contract from
|
|
|
19
18
|
2. **Exchange**: RFC 8693 token exchange, subject = the inbound user token,
|
|
20
19
|
authenticated by the calling agent's credential; the user stays the subject
|
|
21
20
|
and the authorization server appends the caller to the `act` chain
|
|
22
|
-
3. **Invoke**: JSON-RPC `
|
|
23
|
-
token as the bearer credential
|
|
21
|
+
3. **Invoke**: JSON-RPC `SendMessage` (A2A protocol 1.0) against the target
|
|
22
|
+
with the exchanged token as the bearer credential
|
|
24
23
|
|
|
25
24
|
Hosting an agent inside a specific agent framework is out of scope, matching
|
|
26
25
|
the Go SDK's boundary. This gem wraps no A2A SDK.
|
|
@@ -49,10 +48,21 @@ result.agent_card # the card that was discovered on the way
|
|
|
49
48
|
```
|
|
50
49
|
|
|
51
50
|
One call covers all three steps: fetch and cache agent B's card, exchange the
|
|
52
|
-
inbound token for one scoped to B, then send JSON-RPC `
|
|
51
|
+
inbound token for one scoped to B, then send JSON-RPC `SendMessage` with the
|
|
53
52
|
exchanged token as the bearer credential. The user stays the subject across the
|
|
54
53
|
hop; agent B verifies a token whose `sub` is the original user, not this agent.
|
|
55
54
|
|
|
55
|
+
### Protocol version
|
|
56
|
+
|
|
57
|
+
The client speaks A2A protocol 1.0 by default: `SendMessage`, an `A2A-Version:
|
|
58
|
+
1.0` header, `ROLE_USER` roles, untagged text parts, and the invocation URL read
|
|
59
|
+
from the card's JSONRPC `supportedInterfaces` entry. Agents still on protocol
|
|
60
|
+
0.3 do not interoperate with the default; to reach one, pass
|
|
61
|
+
`protocol_version: Keycardai::A2A::LEGACY_PROTOCOL_VERSION`. That switches the
|
|
62
|
+
whole envelope generation (`message/send`, `X-A2A-Protocol-Version: 0.3`,
|
|
63
|
+
`user` roles, `kind`-tagged parts), so `Keycardai::A2A.text_message` output is
|
|
64
|
+
still the input either way. Any other value raises `ArgumentError`.
|
|
65
|
+
|
|
56
66
|
Failures are typed by stage, so you can tell "B is unreachable" from "the zone
|
|
57
67
|
refused the exchange":
|
|
58
68
|
|
|
@@ -24,7 +24,12 @@ module Keycardai
|
|
|
24
24
|
# @param http_client [#get, #post_form, #post_json] pluggable transport
|
|
25
25
|
# @param discovery [ServiceDiscovery, nil] card resolution override
|
|
26
26
|
# @param invoke_timeout [Numeric, nil] JSON-RPC call timeout
|
|
27
|
-
# @param protocol_version [String]
|
|
27
|
+
# @param protocol_version [String] the A2A generation to speak: PROTOCOL_VERSION
|
|
28
|
+
# (1.0: SendMessage, A2A-Version header, ROLE_USER roles, untagged text parts)
|
|
29
|
+
# or LEGACY_PROTOCOL_VERSION (0.3: message/send, X-A2A-Protocol-Version header,
|
|
30
|
+
# "user" roles, kind-tagged parts). Build the message with A2A.text_message
|
|
31
|
+
# either way; the client translates it for 0.3.
|
|
32
|
+
# @raise [ArgumentError] protocol_version is neither supported generation
|
|
28
33
|
def initialize(issuer:, credential: nil, client_id: nil, client_secret: nil,
|
|
29
34
|
http_client: OAuth::HTTP::NetHTTPClient.new, discovery: nil,
|
|
30
35
|
invoke_timeout: nil, protocol_version: PROTOCOL_VERSION)
|
|
@@ -34,15 +39,17 @@ module Keycardai
|
|
|
34
39
|
@discovery = discovery || ServiceDiscovery.new(http_client: http_client)
|
|
35
40
|
@http_client = http_client
|
|
36
41
|
@invoke_timeout = invoke_timeout
|
|
37
|
-
@
|
|
42
|
+
@wire = Wire.for(protocol_version)
|
|
38
43
|
end
|
|
39
44
|
|
|
40
45
|
# Delegate a call to another agent: discover, exchange, invoke.
|
|
41
46
|
#
|
|
42
47
|
# @param target [String] the downstream agent's base URL
|
|
43
48
|
# @param subject_token [String] the inbound user's verified access token
|
|
44
|
-
# @param message [Hash] the A2A
|
|
45
|
-
# @return [Result]
|
|
49
|
+
# @param message [Hash] the A2A SendMessage params (see A2A.text_message)
|
|
50
|
+
# @return [Result] message is the JSON-RPC result as the agent returned it:
|
|
51
|
+
# for a 1.0 agent an object with a +message+ or +task+ key
|
|
52
|
+
# @raise [ArgumentError] protocol_version is unsupported
|
|
46
53
|
# @raise [DiscoveryError] the agent card cannot be resolved
|
|
47
54
|
# @raise [Keycardai::OAuth::OAuthError] the exchange was rejected
|
|
48
55
|
# @raise [InvocationError] the JSON-RPC call failed
|
|
@@ -55,17 +62,18 @@ module Keycardai
|
|
|
55
62
|
|
|
56
63
|
private
|
|
57
64
|
|
|
58
|
-
# The invocation endpoint: read from the card when it names one
|
|
59
|
-
#
|
|
65
|
+
# The invocation endpoint: read from the card when it names one (a 1.0
|
|
66
|
+
# card's JSONRPC interface or a 0.3 card's url), otherwise derived by
|
|
67
|
+
# convention from the target base URL.
|
|
60
68
|
def jsonrpc_url(target, card)
|
|
61
|
-
|
|
69
|
+
@wire.endpoint_from(card) || "#{target.chomp("/")}#{JSONRPC_PATH}"
|
|
62
70
|
end
|
|
63
71
|
|
|
64
72
|
def post_jsonrpc(url, access_token, message)
|
|
65
|
-
payload = { "jsonrpc" => "2.0", "id" => SecureRandom.uuid, "method" =>
|
|
66
|
-
"params" => message }
|
|
73
|
+
payload = { "jsonrpc" => "2.0", "id" => SecureRandom.uuid, "method" => @wire.method_name,
|
|
74
|
+
"params" => @wire.encode_params(message) }
|
|
67
75
|
headers = { "Accept" => "application/json", "Authorization" => "Bearer #{access_token}",
|
|
68
|
-
|
|
76
|
+
@wire.header => @wire.version }
|
|
69
77
|
response = begin
|
|
70
78
|
@http_client.post_json(url, payload, headers: headers, timeout: @invoke_timeout)
|
|
71
79
|
rescue OAuth::NetworkError => e
|
|
@@ -91,16 +99,17 @@ module Keycardai
|
|
|
91
99
|
end
|
|
92
100
|
end
|
|
93
101
|
|
|
94
|
-
# Build A2A
|
|
95
|
-
# for driving a downstream agent.
|
|
102
|
+
# Build A2A 1.0 SendMessage params carrying one text part, the common case
|
|
103
|
+
# for driving a downstream agent. A client configured for 0.3 translates
|
|
104
|
+
# the role and part shape on the way out.
|
|
96
105
|
#
|
|
97
106
|
# @param text [String]
|
|
98
107
|
# @return [Hash]
|
|
99
108
|
def self.text_message(text)
|
|
100
109
|
{
|
|
101
110
|
"message" => {
|
|
102
|
-
"role" =>
|
|
103
|
-
"parts" => [{ "
|
|
111
|
+
"role" => ROLE_USER,
|
|
112
|
+
"parts" => [{ "text" => text }],
|
|
104
113
|
"messageId" => SecureRandom.uuid
|
|
105
114
|
}
|
|
106
115
|
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Keycardai
|
|
4
|
+
module A2A
|
|
5
|
+
# A2A protocol version spoken by default: the 1.0 generation, which is
|
|
6
|
+
# what keycardai-a2a (Python) and @keycardai/a2a (TypeScript) serve.
|
|
7
|
+
PROTOCOL_VERSION = "1.0"
|
|
8
|
+
|
|
9
|
+
# The 0.3 generation, for agents not yet on 1.0. Pass it as
|
|
10
|
+
# DelegationClient's protocol_version: to send 0.3-shaped requests.
|
|
11
|
+
LEGACY_PROTOCOL_VERSION = "0.3"
|
|
12
|
+
|
|
13
|
+
# JSON-RPC method that delivers a message to a 1.0 agent.
|
|
14
|
+
MESSAGE_SEND_METHOD = "SendMessage"
|
|
15
|
+
|
|
16
|
+
# JSON-RPC method that delivers a message to a 0.3 agent.
|
|
17
|
+
LEGACY_MESSAGE_SEND_METHOD = "message/send"
|
|
18
|
+
|
|
19
|
+
# Header carrying the protocol version on a 1.0 request.
|
|
20
|
+
PROTOCOL_VERSION_HEADER = "A2A-Version"
|
|
21
|
+
|
|
22
|
+
# Header carrying the protocol version on a 0.3 request.
|
|
23
|
+
LEGACY_PROTOCOL_VERSION_HEADER = "X-A2A-Protocol-Version"
|
|
24
|
+
|
|
25
|
+
# Message roles as the 1.0 protocol names them (0.3 used "user"/"agent").
|
|
26
|
+
ROLE_USER = "ROLE_USER"
|
|
27
|
+
ROLE_AGENT = "ROLE_AGENT"
|
|
28
|
+
|
|
29
|
+
# One protocol generation's wire conventions: the JSON-RPC method, the
|
|
30
|
+
# version header, and how message roles and text parts are spelled. The
|
|
31
|
+
# version selects the whole envelope, never only the header.
|
|
32
|
+
class Wire
|
|
33
|
+
LEGACY_ROLES = { ROLE_USER => "user", ROLE_AGENT => "agent" }.freeze
|
|
34
|
+
|
|
35
|
+
# @param version [String] PROTOCOL_VERSION or LEGACY_PROTOCOL_VERSION
|
|
36
|
+
# @raise [ArgumentError] any other value
|
|
37
|
+
def self.for(version)
|
|
38
|
+
case version
|
|
39
|
+
when PROTOCOL_VERSION
|
|
40
|
+
new(version: version, header: PROTOCOL_VERSION_HEADER, method_name: MESSAGE_SEND_METHOD)
|
|
41
|
+
when LEGACY_PROTOCOL_VERSION
|
|
42
|
+
new(version: version, header: LEGACY_PROTOCOL_VERSION_HEADER, method_name: LEGACY_MESSAGE_SEND_METHOD)
|
|
43
|
+
else
|
|
44
|
+
raise ArgumentError, "unsupported A2A protocol version #{version.inspect} " \
|
|
45
|
+
"(supported: #{PROTOCOL_VERSION}, #{LEGACY_PROTOCOL_VERSION})"
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
attr_reader :version, :header, :method_name
|
|
50
|
+
|
|
51
|
+
def initialize(version:, header:, method_name:)
|
|
52
|
+
@version = version
|
|
53
|
+
@header = header
|
|
54
|
+
@method_name = method_name
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def legacy?
|
|
58
|
+
version == LEGACY_PROTOCOL_VERSION
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Encode 1.0-shaped params (as built by A2A.text_message) for the wire.
|
|
62
|
+
# On 1.0 they pass through; on 0.3 the message role takes its 0.3 name
|
|
63
|
+
# and text parts gain the kind tag 0.3 requires.
|
|
64
|
+
def encode_params(params)
|
|
65
|
+
message = params.is_a?(Hash) ? params["message"] : nil
|
|
66
|
+
return params unless legacy? && message.is_a?(Hash)
|
|
67
|
+
|
|
68
|
+
params.merge("message" => message.merge(
|
|
69
|
+
"role" => LEGACY_ROLES.fetch(message["role"], message["role"]),
|
|
70
|
+
"parts" => Array(message["parts"]).map { |part| legacy_part(part) }
|
|
71
|
+
))
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# The invocation endpoint from an agent card: a 1.0 card's JSONRPC
|
|
75
|
+
# interface (preferring the one matching this version), else a 0.3
|
|
76
|
+
# card's url, else nil.
|
|
77
|
+
def endpoint_from(card)
|
|
78
|
+
interfaces = Array(card["supportedInterfaces"]).select { |iface| jsonrpc_interface?(iface) }
|
|
79
|
+
matching = interfaces.find { |iface| iface["protocolVersion"] == version } || interfaces.first
|
|
80
|
+
return matching["url"] if matching
|
|
81
|
+
|
|
82
|
+
card["url"] if card["url"].is_a?(String) && !card["url"].empty?
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
private
|
|
86
|
+
|
|
87
|
+
def legacy_part(part)
|
|
88
|
+
return part unless part.is_a?(Hash) && part.key?("text") && !part.key?("kind")
|
|
89
|
+
|
|
90
|
+
{ "kind" => "text" }.merge(part)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def jsonrpc_interface?(iface)
|
|
94
|
+
iface.is_a?(Hash) && iface["protocolBinding"].to_s.casecmp?("JSONRPC") &&
|
|
95
|
+
iface["url"].is_a?(String) && !iface["url"].empty?
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
data/lib/keycardai/a2a.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "keycardai/oauth"
|
|
4
4
|
require_relative "a2a/version"
|
|
5
5
|
require_relative "a2a/errors"
|
|
6
|
+
require_relative "a2a/wire"
|
|
6
7
|
require_relative "a2a/service_discovery"
|
|
7
8
|
require_relative "a2a/delegation_client"
|
|
8
9
|
|
|
@@ -21,10 +22,7 @@ module Keycardai
|
|
|
21
22
|
# JSON-RPC invocation path, relative to an agent's base URL.
|
|
22
23
|
JSONRPC_PATH = "/a2a/jsonrpc"
|
|
23
24
|
|
|
24
|
-
#
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
# A2A protocol version sent with each invocation.
|
|
28
|
-
PROTOCOL_VERSION = "0.3"
|
|
25
|
+
# The protocol-generation constants (PROTOCOL_VERSION, MESSAGE_SEND_METHOD,
|
|
26
|
+
# ROLE_USER, and their LEGACY_ 0.3 counterparts) live in a2a/wire.rb.
|
|
29
27
|
end
|
|
30
28
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: keycardai-a2a
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Keycard
|
|
@@ -40,6 +40,7 @@ files:
|
|
|
40
40
|
- lib/keycardai/a2a/errors.rb
|
|
41
41
|
- lib/keycardai/a2a/service_discovery.rb
|
|
42
42
|
- lib/keycardai/a2a/version.rb
|
|
43
|
+
- lib/keycardai/a2a/wire.rb
|
|
43
44
|
homepage: https://github.com/keycardai/ruby-sdk
|
|
44
45
|
licenses:
|
|
45
46
|
- MIT
|