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
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: b0b5296863ce5b960cf2b4a3f27c3f4e4e93399f93c13c003719437e62891582
|
|
4
|
+
data.tar.gz: 52641e6cf375bcf43392e178b3e06aa21e8dc77a2eda334ee3bb7e33ce598453
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 99fbf4d17ac3d536da464f3893174c459ad63822e967b9c798840770eee9f255d72fc2270a90a4d3ef8b692fc943c902314705da510d23d181f73f4df480155d
|
|
7
|
+
data.tar.gz: 700d3a88eebecb34044067b1b55eb89b74d4c3ba11840ffc205c0e8f3027ccdc8389599d6fe2d0943c62db23f182ec738c96e5d3cd7f21b8fe212f31370ed5eb
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.1.1] — 2026-06-26\n\n### Added
|
|
11
|
+
- wire up top-level A2A module with error classes and requires
|
|
12
|
+
- add JSONRPCEnvelope for server-side request/response handling
|
|
13
|
+
- add JSON-RPC and HTTP+JSON protocol bindings and client
|
|
14
|
+
- add push notification config, dispatcher, and Rack receiver
|
|
15
|
+
- add security schemes and OAuth flow types
|
|
16
|
+
- add AgentCard, discovery, and agent metadata types
|
|
17
|
+
- add streaming types and SSE parser/writer
|
|
18
|
+
- add core data model (Task, Message, Artifact, Part types, Role)
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
- pre-publish fixes (gemspec author, URLs, license, dev deps to Gemfile)
|
|
22
|
+
- add conventional commits hook and automated changelog generation (release)
|
|
23
|
+
- add rubocop config, ruby version pin, and gemspec
|
|
24
|
+
|
|
25
|
+
### Other
|
|
26
|
+
- update CLAUDE.md
|
|
27
|
+
- add README, examples, changelog, and Claude harness
|
|
28
|
+
- add full spec suite (595 examples)
|
|
29
|
+
|
|
30
|
+
## [0.1.0] — 2026-06-17
|
|
31
|
+
|
|
32
|
+
### Added
|
|
33
|
+
- Initial gem scaffold (lib, spec, bin/console, bin/setup, gemspec)
|
|
34
|
+
|
|
35
|
+
[Unreleased]: https://github.com/rafaelqfigueiredo/a2a-rb/compare/v0.1.1...HEAD
|
|
36
|
+
[0.1.0]: https://github.com/rafaelqfigueiredo/a2a-rb/releases/tag/v0.1.0
|
|
37
|
+
|
|
38
|
+
[0.1.1]: https://github.com/rafaelqfigueiredo/a2a-rb/compare/v0.1.0...v0.1.1
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Rafael Figueiredo
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
# a2a-rb
|
|
2
|
+
|
|
3
|
+
Ruby implementation of the [A2A protocol v1.0](https://a2a-protocol.org/latest/specification) — an open standard for agent-to-agent communication.
|
|
4
|
+
|
|
5
|
+
The gem is a **data-model and serialisation library**: it models every message
|
|
6
|
+
type from the A2A spec, provides a full client for calling remote agents, and
|
|
7
|
+
includes protocol-level primitives for building server-side handlers. No HTTP
|
|
8
|
+
server is bundled — mount it behind Rails, Sinatra, or any Rack application.
|
|
9
|
+
|
|
10
|
+
- Ruby 3.4+
|
|
11
|
+
- No runtime dependencies
|
|
12
|
+
- Both JSON-RPC 2.0 and HTTP+JSON bindings
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```ruby
|
|
17
|
+
# Gemfile
|
|
18
|
+
gem "a2a-rb"
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
bundle install
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Quick start
|
|
26
|
+
|
|
27
|
+
```ruby
|
|
28
|
+
require "a2a"
|
|
29
|
+
|
|
30
|
+
# 1. Discover an agent
|
|
31
|
+
card = A2A::Discovery.fetch("https://agent.example.com")
|
|
32
|
+
|
|
33
|
+
# 2. Build a client (negotiates the best available protocol binding)
|
|
34
|
+
client = A2A::Client.from_agent_card(card)
|
|
35
|
+
|
|
36
|
+
# 3. Send a message
|
|
37
|
+
result = client.send_message(
|
|
38
|
+
A2A::Message.new(
|
|
39
|
+
id: SecureRandom.uuid,
|
|
40
|
+
role: A2A::Role::USER,
|
|
41
|
+
parts: [A2A::Part::Text.new(text: "Summarise this document.")]
|
|
42
|
+
)
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
case result
|
|
46
|
+
when A2A::Task then puts "Task #{result.id}: #{result.status.state}"
|
|
47
|
+
when A2A::Message then puts result.parts.first.text
|
|
48
|
+
end
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## What's included
|
|
52
|
+
|
|
53
|
+
### Client
|
|
54
|
+
|
|
55
|
+
`A2A::Client` covers all eleven A2A JSON-RPC methods:
|
|
56
|
+
|
|
57
|
+
| Method | Client call |
|
|
58
|
+
|--------|-------------|
|
|
59
|
+
| `SendMessage` | `client.send_message(message, configuration:, metadata:, tenant:)` |
|
|
60
|
+
| `SendStreamingMessage` | `client.send_streaming_message(message, ...) { \|event\| }` |
|
|
61
|
+
| `GetTask` | `client.get_task(id, history_length:)` |
|
|
62
|
+
| `ListTasks` | `client.list_tasks(page_size:, page_token:, status:, ...)` |
|
|
63
|
+
| `CancelTask` | `client.cancel_task(id_or_task)` |
|
|
64
|
+
| `SubscribeToTask` | `client.subscribe_to_task(id) { \|event\| }` |
|
|
65
|
+
| `CreatePushNotificationConfig` | `client.create_task_push_notification_config(config)` |
|
|
66
|
+
| `GetPushNotificationConfig` | `client.get_task_push_notification_config(task_id:, id:)` |
|
|
67
|
+
| `ListPushNotificationConfigs` | `client.list_task_push_notification_configs(task_id:)` |
|
|
68
|
+
| `DeletePushNotificationConfig` | `client.delete_task_push_notification_config(task_id:, id:)` |
|
|
69
|
+
| `GetExtendedAgentCard` | `client.get_extended_agent_card` |
|
|
70
|
+
|
|
71
|
+
The client accepts both a `Task` object and a plain string ID for operations
|
|
72
|
+
that reference tasks. Passing a terminal `Task` to `cancel_task` raises
|
|
73
|
+
`TaskNotCancelableError` locally without a network round-trip.
|
|
74
|
+
|
|
75
|
+
### Protocol bindings
|
|
76
|
+
|
|
77
|
+
```ruby
|
|
78
|
+
# JSON-RPC 2.0
|
|
79
|
+
protocol = A2A::Protocol::JsonRpc.new(
|
|
80
|
+
url: "https://agent.example.com/rpc",
|
|
81
|
+
headers: { "Authorization" => "Bearer token" }
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# HTTP+JSON (REST-style)
|
|
85
|
+
protocol = A2A::Protocol::HttpJson.new(
|
|
86
|
+
url: "https://agent.example.com",
|
|
87
|
+
extensions: ["https://ext.example.com/v1"]
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
client = A2A::Client.new(protocol: protocol)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Data model
|
|
94
|
+
|
|
95
|
+
All message types implement `.from_h(hash)` / `#to_h` for lossless
|
|
96
|
+
round-trip serialisation against the A2A wire format.
|
|
97
|
+
|
|
98
|
+
| Class | Purpose |
|
|
99
|
+
|-------|---------|
|
|
100
|
+
| `Task` | Unit of work; carries `id`, `status`, `artifacts`, `history` |
|
|
101
|
+
| `Task::Status` | State + optional message + timestamp |
|
|
102
|
+
| `Task::State` | String constants + `TERMINAL` / `RESUMABLE` collections |
|
|
103
|
+
| `Message` | User↔agent communication unit; carries `parts` |
|
|
104
|
+
| `Artifact` | Task output (not for communication) |
|
|
105
|
+
| `Part::Text` | Plain text content |
|
|
106
|
+
| `Part::Data` | Structured JSON content |
|
|
107
|
+
| `Part::File` | File by URL or base64 inline (`raw`/`url`, `filename`, `media_type`) |
|
|
108
|
+
| `Role` | `ROLE_USER` / `ROLE_AGENT` constants |
|
|
109
|
+
|
|
110
|
+
### Streaming
|
|
111
|
+
|
|
112
|
+
```ruby
|
|
113
|
+
client.send_streaming_message(message) do |event|
|
|
114
|
+
case event.type
|
|
115
|
+
when :status_update then puts event.payload.status.state
|
|
116
|
+
when :artifact_update then print event.payload.artifact.parts.first.text
|
|
117
|
+
when :task then puts "snapshot: #{event.payload.status.state}"
|
|
118
|
+
when :message then puts event.payload.parts.first.text
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Streaming stops automatically when a terminal state is detected. Without a
|
|
124
|
+
block, `send_streaming_message` returns a `Streaming::Subscription` that is
|
|
125
|
+
`Enumerable`.
|
|
126
|
+
|
|
127
|
+
### Server-side primitives
|
|
128
|
+
|
|
129
|
+
The gem includes three classes for building server handlers:
|
|
130
|
+
|
|
131
|
+
| Class | Purpose |
|
|
132
|
+
|-------|---------|
|
|
133
|
+
| `JSONRPCEnvelope` | Build success/error response envelopes; parse incoming request envelopes |
|
|
134
|
+
| `Operation::SendMessageRequest` | Deserialise incoming `SendMessage`/`SendStreamingMessage` params |
|
|
135
|
+
| `Streaming::SSEWriter` | Format `Streaming::Response` objects as SSE frames |
|
|
136
|
+
|
|
137
|
+
```ruby
|
|
138
|
+
# Parse an incoming request
|
|
139
|
+
id, method, params = A2A::JSONRPCEnvelope.parse_request(raw_hash)
|
|
140
|
+
req = A2A::Operation::SendMessageRequest.from_h(params)
|
|
141
|
+
|
|
142
|
+
# Build a response
|
|
143
|
+
A2A::JSONRPCEnvelope.success(id: id, result: { "task" => task.to_h })
|
|
144
|
+
|
|
145
|
+
# Write an SSE frame
|
|
146
|
+
A2A::Streaming::SSEWriter.encode(streaming_response, id: id)
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Task transitions (server-side)
|
|
150
|
+
|
|
151
|
+
`Task#transition_to` returns a new immutable `Task` — the original is never
|
|
152
|
+
mutated. Raises `TaskNotCancelableError` if the task is already terminal.
|
|
153
|
+
|
|
154
|
+
```ruby
|
|
155
|
+
task = task.transition_to(A2A::Task::State::WORKING)
|
|
156
|
+
task = task.transition_to(A2A::Task::State::COMPLETED, timestamp: Time.now.utc.iso8601)
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### AgentCard builder
|
|
160
|
+
|
|
161
|
+
```ruby
|
|
162
|
+
card = A2A::AgentCard::Builder.new
|
|
163
|
+
.name("My Agent")
|
|
164
|
+
.description("Does useful things.")
|
|
165
|
+
.version("1.0")
|
|
166
|
+
.interface(url: "https://agent.example.com/rpc",
|
|
167
|
+
protocol_binding: A2A::AgentInterface::JSONRPC,
|
|
168
|
+
protocol_version: "1.0")
|
|
169
|
+
.capabilities(streaming: true, push_notifications: true)
|
|
170
|
+
.input_modes("text/plain")
|
|
171
|
+
.output_modes("text/plain")
|
|
172
|
+
.skill(id: "summarise", name: "Summarise",
|
|
173
|
+
description: "Summarises documents", tags: ["text"])
|
|
174
|
+
.build
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### Push notifications
|
|
178
|
+
|
|
179
|
+
```ruby
|
|
180
|
+
# Dispatch from a server
|
|
181
|
+
dispatcher = A2A::PushNotification::Dispatcher.new
|
|
182
|
+
dispatcher.dispatch(config, streaming_response)
|
|
183
|
+
|
|
184
|
+
# Receive in a Rack app
|
|
185
|
+
use A2A::PushNotification::Receiver,
|
|
186
|
+
path: "/a2a/webhook",
|
|
187
|
+
credentials: ENV["WEBHOOK_TOKEN"] do |event|
|
|
188
|
+
MyJob.perform_later(event.to_h.to_json)
|
|
189
|
+
end
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### Security schemes
|
|
193
|
+
|
|
194
|
+
All five A2A security scheme types are modelled: `HTTPAuth`, `APIKey`,
|
|
195
|
+
`OAuth2` (authorization code, client credentials, device code),
|
|
196
|
+
`OpenIDConnect`, and `MutualTLS`. `SecurityScheme.from_h` dispatches on
|
|
197
|
+
the discriminator key.
|
|
198
|
+
|
|
199
|
+
### Errors
|
|
200
|
+
|
|
201
|
+
All errors inherit from `A2A::Error` and carry a `code` integer matching
|
|
202
|
+
the A2A spec's JSON-RPC error codes. `A2A.from_json_rpc_error(hash)` builds
|
|
203
|
+
the correct subclass from a raw error hash.
|
|
204
|
+
|
|
205
|
+
```
|
|
206
|
+
A2A::Error
|
|
207
|
+
├── A2A::TransportError
|
|
208
|
+
├── A2A::AuthenticationError # HTTP 401
|
|
209
|
+
├── A2A::AuthorizationError # HTTP 403
|
|
210
|
+
├── A2A::TaskNotFoundError # -32001
|
|
211
|
+
├── A2A::TaskNotCancelableError # -32002
|
|
212
|
+
├── A2A::PushNotificationNotSupportedError # -32003
|
|
213
|
+
├── A2A::UnsupportedOperationError # -32004
|
|
214
|
+
├── A2A::ContentTypeNotSupportedError # -32005
|
|
215
|
+
├── A2A::VersionNotSupportedError # -32009
|
|
216
|
+
└── ... (full list in lib/a2a.rb)
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
## Examples
|
|
220
|
+
|
|
221
|
+
The [`examples/`](examples/) folder contains worked examples for each area of
|
|
222
|
+
the gem:
|
|
223
|
+
|
|
224
|
+
| File | Topic |
|
|
225
|
+
|------|-------|
|
|
226
|
+
| [`01_discovery.md`](examples/01_discovery.md) | Fetch an agent card; build a client from it |
|
|
227
|
+
| [`02_send_message.md`](examples/02_send_message.md) | Send text, file, and data messages; configuration; error handling |
|
|
228
|
+
| [`03_streaming.md`](examples/03_streaming.md) | Receive and emit SSE streams; `SSEWriter` |
|
|
229
|
+
| [`04_task_lifecycle.md`](examples/04_task_lifecycle.md) | Fetch, list, cancel, and transition tasks |
|
|
230
|
+
| [`05_push_notifications.md`](examples/05_push_notifications.md) | Register configs; dispatch and receive push events |
|
|
231
|
+
| [`06_agent_card.md`](examples/06_agent_card.md) | Declare and serve an `AgentCard` |
|
|
232
|
+
| [`07_server_side.md`](examples/07_server_side.md) | Parse requests; build responses; stream SSE from a Rack handler |
|
|
233
|
+
| [`08_security_schemes.md`](examples/08_security_schemes.md) | All five security scheme types; attach to a card |
|
|
234
|
+
|
|
235
|
+
## Development
|
|
236
|
+
|
|
237
|
+
```bash
|
|
238
|
+
bin/setup # install dependencies
|
|
239
|
+
bin/console # open a pry REPL with A2A loaded
|
|
240
|
+
bundle exec rake # run the test suite
|
|
241
|
+
bin/install-hooks # install the commit-msg hook (conventional commits)
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
## Releasing a new version
|
|
245
|
+
|
|
246
|
+
Commits must follow [Conventional Commits](https://www.conventionalcommits.org/).
|
|
247
|
+
Install the local commit-msg hook once after cloning:
|
|
248
|
+
|
|
249
|
+
```bash
|
|
250
|
+
bin/install-hooks
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
Valid types: `feat` (Added), `fix` (Fixed), `refactor`/`chore` (Changed),
|
|
254
|
+
`perf`, `revert`/`remove`, `deprecate`, `security`. Other types land in Other.
|
|
255
|
+
Merge commits and `Release vX.Y.Z` commits are exempt.
|
|
256
|
+
|
|
257
|
+
### Run the release script
|
|
258
|
+
|
|
259
|
+
```bash
|
|
260
|
+
bin/release patch # 0.1.0 → 0.1.1
|
|
261
|
+
bin/release minor # 0.1.0 → 0.2.0
|
|
262
|
+
bin/release major # 0.1.0 → 1.0.0
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
The script will:
|
|
266
|
+
|
|
267
|
+
1. Abort if the working tree has uncommitted changes
|
|
268
|
+
2. Run the full test suite (`bundle exec rake spec`)
|
|
269
|
+
3. Parse `git log` since the previous tag and group commits by type
|
|
270
|
+
4. Show you the draft changelog entry and the new version, then ask for confirmation
|
|
271
|
+
5. Write the changelog entry into `CHANGELOG.md`
|
|
272
|
+
6. Bump `lib/a2a/version.rb`
|
|
273
|
+
7. Commit both files with the message `Release vx.y.z`
|
|
274
|
+
8. Create an annotated git tag `vx.y.z`
|
|
275
|
+
|
|
276
|
+
Aborts if no conventional commits are found since the last tag.
|
|
277
|
+
|
|
278
|
+
### 3. Push and publish
|
|
279
|
+
|
|
280
|
+
```bash
|
|
281
|
+
git push origin main vx.y.z # push the commit and the tag together
|
|
282
|
+
|
|
283
|
+
bundle exec rake build # builds pkg/a2a-rb-x.y.z.gem
|
|
284
|
+
gem push pkg/a2a-rb-x.y.z.gem # publish to RubyGems (requires credentials)
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
> RubyGems MFA is enforced for this gem. You will be prompted for a one-time
|
|
288
|
+
> password when running `gem push`.
|
|
289
|
+
|
|
290
|
+
### Preflight check (optional)
|
|
291
|
+
|
|
292
|
+
```bash
|
|
293
|
+
bundle exec rake preflight
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
Runs specs, checks that `[Unreleased]` is not empty, and verifies the working
|
|
297
|
+
tree is clean.
|
|
298
|
+
|
|
299
|
+
---
|
|
300
|
+
|
|
301
|
+
### Quick reference
|
|
302
|
+
|
|
303
|
+
| Command | What it does |
|
|
304
|
+
|---------|--------------|
|
|
305
|
+
| `bin/release patch` | Bump patch, update changelog, commit, tag |
|
|
306
|
+
| `bin/release minor` | Bump minor, update changelog, commit, tag |
|
|
307
|
+
| `bin/release major` | Bump major, update changelog, commit, tag |
|
|
308
|
+
| `bundle exec rake preflight` | Specs + changelog check + clean tree |
|
|
309
|
+
| `bundle exec rake build` | Build `.gem` into `pkg/` |
|
|
310
|
+
| `gem push pkg/a2a-rb-x.y.z.gem` | Publish to RubyGems |
|
|
311
|
+
| `bundle exec rake spec` | Run tests only |
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module A2A
|
|
4
|
+
class AgentCapabilities
|
|
5
|
+
attr_reader :streaming, :push_notifications, :extensions, :extended_agent_card
|
|
6
|
+
|
|
7
|
+
def initialize(streaming: nil, push_notifications: nil, extensions: nil, extended_agent_card: nil)
|
|
8
|
+
@streaming = streaming
|
|
9
|
+
@push_notifications = push_notifications
|
|
10
|
+
@extensions = extensions
|
|
11
|
+
@extended_agent_card = extended_agent_card
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.from_h(hash)
|
|
15
|
+
new(
|
|
16
|
+
streaming: hash["streaming"],
|
|
17
|
+
push_notifications: hash["pushNotifications"],
|
|
18
|
+
extensions: hash["extensions"]&.map { AgentExtension.from_h(_1) },
|
|
19
|
+
extended_agent_card: hash["extendedAgentCard"]
|
|
20
|
+
)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def to_h
|
|
24
|
+
{
|
|
25
|
+
"streaming" => streaming,
|
|
26
|
+
"pushNotifications" => push_notifications,
|
|
27
|
+
"extensions" => extensions,
|
|
28
|
+
"extendedAgentCard" => extended_agent_card
|
|
29
|
+
}.compact
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module A2A
|
|
4
|
+
class AgentCard
|
|
5
|
+
# Fluent builder for constructing an AgentCard without deep nested constructors.
|
|
6
|
+
#
|
|
7
|
+
# Usage:
|
|
8
|
+
# card = A2A::AgentCard::Builder.new
|
|
9
|
+
# .name("My Agent")
|
|
10
|
+
# .description("Does things")
|
|
11
|
+
# .version("1.0")
|
|
12
|
+
# .interface(url: "https://agent.example.com/rpc", protocol_binding: A2A::AgentInterface::JSONRPC)
|
|
13
|
+
# .capabilities(streaming: true)
|
|
14
|
+
# .input_modes("text/plain")
|
|
15
|
+
# .output_modes("text/plain")
|
|
16
|
+
# .skill(id: "summarise", name: "Summarise", description: "Summarises text", tags: ["text"])
|
|
17
|
+
# .build
|
|
18
|
+
class Builder
|
|
19
|
+
def initialize
|
|
20
|
+
@name = nil
|
|
21
|
+
@description = nil
|
|
22
|
+
@version = nil
|
|
23
|
+
@interfaces = []
|
|
24
|
+
@capabilities_opts = {}
|
|
25
|
+
@input_modes = []
|
|
26
|
+
@output_modes = []
|
|
27
|
+
@skills = []
|
|
28
|
+
@provider = nil
|
|
29
|
+
@documentation_url = nil
|
|
30
|
+
@icon_url = nil
|
|
31
|
+
@security_schemes = {}
|
|
32
|
+
@security_requirements = nil
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def name(value)
|
|
36
|
+
@name = value
|
|
37
|
+
self
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def description(value)
|
|
41
|
+
@description = value
|
|
42
|
+
self
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def version(value)
|
|
46
|
+
@version = value
|
|
47
|
+
self
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Appends a supported interface. Accepts either an AgentInterface object
|
|
51
|
+
# or kwargs forwarded to AgentInterface.new.
|
|
52
|
+
def interface(iface = nil, **kwargs)
|
|
53
|
+
@interfaces << (iface.is_a?(AgentInterface) ? iface : AgentInterface.new(**kwargs))
|
|
54
|
+
self
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Sets capabilities via kwargs (streaming:, push_notifications:, etc.)
|
|
58
|
+
# or accepts an AgentCapabilities object directly.
|
|
59
|
+
def capabilities(caps = nil, **kwargs)
|
|
60
|
+
@capabilities_opts = caps.is_a?(AgentCapabilities) ? caps : kwargs
|
|
61
|
+
self
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Appends one or more accepted input MIME types.
|
|
65
|
+
def input_modes(*modes)
|
|
66
|
+
@input_modes.concat(modes.flatten)
|
|
67
|
+
self
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Appends one or more accepted output MIME types.
|
|
71
|
+
def output_modes(*modes)
|
|
72
|
+
@output_modes.concat(modes.flatten)
|
|
73
|
+
self
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Appends a skill. Accepts either an AgentSkill object or kwargs
|
|
77
|
+
# forwarded to AgentSkill.new.
|
|
78
|
+
def skill(obj = nil, **kwargs)
|
|
79
|
+
@skills << (obj.is_a?(AgentSkill) ? obj : AgentSkill.new(**kwargs))
|
|
80
|
+
self
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def provider(org, url: nil)
|
|
84
|
+
@provider = AgentProvider.new(organization: org, url: url)
|
|
85
|
+
self
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def documentation_url(value)
|
|
89
|
+
@documentation_url = value
|
|
90
|
+
self
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def icon_url(value)
|
|
94
|
+
@icon_url = value
|
|
95
|
+
self
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Registers a security scheme under `name`. Accepts a SecurityScheme
|
|
99
|
+
# subclass or a raw hash forwarded to SecurityScheme.from_h.
|
|
100
|
+
def security_scheme(name, scheme)
|
|
101
|
+
@security_schemes[name] = scheme.is_a?(Hash) ? SecurityScheme.from_h(scheme) : scheme
|
|
102
|
+
self
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def security(requirements)
|
|
106
|
+
@security_requirements = requirements
|
|
107
|
+
self
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def build
|
|
111
|
+
AgentCard.new(
|
|
112
|
+
name: @name,
|
|
113
|
+
description: @description,
|
|
114
|
+
version: @version,
|
|
115
|
+
supported_interfaces: @interfaces,
|
|
116
|
+
capabilities: resolve_capabilities,
|
|
117
|
+
default_input_modes: @input_modes,
|
|
118
|
+
default_output_modes: @output_modes,
|
|
119
|
+
skills: @skills,
|
|
120
|
+
provider: @provider,
|
|
121
|
+
documentation_url: @documentation_url,
|
|
122
|
+
icon_url: @icon_url,
|
|
123
|
+
security_schemes: @security_schemes.empty? ? nil : @security_schemes,
|
|
124
|
+
security_requirements: @security_requirements
|
|
125
|
+
)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
private
|
|
129
|
+
|
|
130
|
+
def resolve_capabilities
|
|
131
|
+
@capabilities_opts.is_a?(AgentCapabilities) ? @capabilities_opts : AgentCapabilities.new(**@capabilities_opts)
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module A2A
|
|
4
|
+
class AgentCard
|
|
5
|
+
# §8.4.2 — JWS signature attached to an AgentCard.
|
|
6
|
+
# `protected` is the base64url-encoded JWS Protected Header.
|
|
7
|
+
# `signature` is the base64url-encoded signature value.
|
|
8
|
+
# `header` is the optional JWS Unprotected Header (plain JSON object, not encoded).
|
|
9
|
+
class Signature
|
|
10
|
+
attr_reader :protected_header, :signature, :header
|
|
11
|
+
|
|
12
|
+
def initialize(protected_header:, signature:, header: nil)
|
|
13
|
+
@protected_header = protected_header
|
|
14
|
+
@signature = signature
|
|
15
|
+
@header = header
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.from_h(hash)
|
|
19
|
+
new(
|
|
20
|
+
protected_header: hash.fetch("protected"),
|
|
21
|
+
signature: hash.fetch("signature"),
|
|
22
|
+
header: hash["header"]
|
|
23
|
+
)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def to_h
|
|
27
|
+
{
|
|
28
|
+
"protected" => protected_header,
|
|
29
|
+
"signature" => signature,
|
|
30
|
+
"header" => header
|
|
31
|
+
}.compact
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module A2A
|
|
4
|
+
class AgentCard
|
|
5
|
+
# §8.4.3 — Verifies AgentCard signatures.
|
|
6
|
+
# Signing is optional; unsigned cards are considered valid.
|
|
7
|
+
# Full JWS verification (RFC 7515 + RFC 8785 canonicalisation) is not yet
|
|
8
|
+
# implemented. See §8.4 of the A2A spec for the required algorithm.
|
|
9
|
+
class Verifier
|
|
10
|
+
def self.verify!(card)
|
|
11
|
+
return true if card.signatures.nil? || card.signatures.empty?
|
|
12
|
+
|
|
13
|
+
raise NotImplementedError,
|
|
14
|
+
"AgentCard signature verification is not yet implemented (§8.4). " \
|
|
15
|
+
"The card carries #{card.signatures.length} signature(s) but they cannot be verified."
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|