keycardai-a2a
Agent-to-agent delegation for Ruby agents on the Keycard platform.
Preview. APIs may change between minor versions while the surface settles. Conformance against the cross-SDK contract is tracked in the conformance report.
bundle add keycardai-a2a
Implements the delegation contract from
keycard-sdk-spec
(specs/a2a/a2a-delegation.md):
- Discover: fetch and cache the target agent's card from
/.well-known/agent-card.json - Exchange: RFC 8693 token exchange, subject = the inbound user token,
authenticated by the calling agent's credential; the user stays the subject
and the authorization server appends the caller to the
actchain - Invoke: JSON-RPC
message/sendagainst the target with the exchanged token as the bearer credential
Hosting an agent inside a specific agent framework is out of scope, matching the Go SDK's boundary. This gem wraps no A2A SDK.
Quickstart
Call another agent on the user's behalf
require "keycardai/a2a"
delegation = Keycardai::A2A::DelegationClient.new(
issuer: ENV.fetch("KEYCARD_URL"),
client_id: ENV.fetch("KEYCARD_CLIENT_ID"),
client_secret: ENV.fetch("KEYCARD_CLIENT_SECRET"),
)
result = delegation.invoke(
target: "https://agent-b.example.com",
subject_token: inbound_user_token,
message: Keycardai::A2A.("summarize today's incidents"),
)
result. # the JSON-RPC result from agent B
result.agent_card # the card that was discovered on the way
One call covers all three steps: fetch and cache agent B's card, exchange the
inbound token for one scoped to B, then send JSON-RPC message/send with the
exchanged token as the bearer credential. The user stays the subject across the
hop; agent B verifies a token whose sub is the original user, not this agent.
Failures are typed by stage, so you can tell "B is unreachable" from "the zone refused the exchange":
begin
delegation.invoke(...)
rescue Keycardai::A2A::DiscoveryError => e # no resolvable agent card
rescue Keycardai::OAuth::OAuthError => e # the zone rejected the exchange
rescue Keycardai::A2A::InvocationError => e # B was reached and failed
end
Discover a card without invoking
discovery = Keycardai::A2A::ServiceDiscovery.new
card = discovery.get_card("https://agent-b.example.com")
card["url"]
Cards are cached for 15 minutes by default (cache_ttl:). refresh forces a
fetch, clear_cache drops everything. Passing the same ServiceDiscovery into
DelegationClient.new(discovery:) shares one cache across both paths.