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.
Files changed (72) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +38 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +311 -0
  5. data/lib/a2a/agent_capabilities.rb +32 -0
  6. data/lib/a2a/agent_card/builder.rb +135 -0
  7. data/lib/a2a/agent_card/signature.rb +35 -0
  8. data/lib/a2a/agent_card/verifier.rb +19 -0
  9. data/lib/a2a/agent_card.rb +118 -0
  10. data/lib/a2a/agent_extension.rb +32 -0
  11. data/lib/a2a/agent_interface.rb +54 -0
  12. data/lib/a2a/agent_provider.rb +20 -0
  13. data/lib/a2a/agent_skill.rb +46 -0
  14. data/lib/a2a/artifact.rb +40 -0
  15. data/lib/a2a/client.rb +109 -0
  16. data/lib/a2a/discovery.rb +49 -0
  17. data/lib/a2a/json_rpc_envelope.rb +32 -0
  18. data/lib/a2a/message.rb +54 -0
  19. data/lib/a2a/oauth_flow/authorization_code.rb +37 -0
  20. data/lib/a2a/oauth_flow/client_credentials.rb +31 -0
  21. data/lib/a2a/oauth_flow/device_code.rb +34 -0
  22. data/lib/a2a/oauth_flow.rb +37 -0
  23. data/lib/a2a/operation/cancel_task.rb +39 -0
  24. data/lib/a2a/operation/create_task_push_notification_config.rb +38 -0
  25. data/lib/a2a/operation/delete_task_push_notification_config.rb +38 -0
  26. data/lib/a2a/operation/executable.rb +27 -0
  27. data/lib/a2a/operation/get_extended_agent_card.rb +32 -0
  28. data/lib/a2a/operation/get_task.rb +39 -0
  29. data/lib/a2a/operation/get_task_push_notification_config.rb +38 -0
  30. data/lib/a2a/operation/list_task_push_notification_configs.rb +64 -0
  31. data/lib/a2a/operation/list_tasks.rb +78 -0
  32. data/lib/a2a/operation/send_message/configuration.rb +39 -0
  33. data/lib/a2a/operation/send_message.rb +58 -0
  34. data/lib/a2a/operation/send_message_request.rb +37 -0
  35. data/lib/a2a/operation/send_streaming_message.rb +53 -0
  36. data/lib/a2a/operation/subscribe_to_task.rb +40 -0
  37. data/lib/a2a/operation.rb +19 -0
  38. data/lib/a2a/part/data.rb +34 -0
  39. data/lib/a2a/part/file.rb +45 -0
  40. data/lib/a2a/part/text.rb +34 -0
  41. data/lib/a2a/part.rb +21 -0
  42. data/lib/a2a/protocol/http_json/transport.rb +82 -0
  43. data/lib/a2a/protocol/http_json.rb +53 -0
  44. data/lib/a2a/protocol/json_rpc/transport.rb +54 -0
  45. data/lib/a2a/protocol/json_rpc.rb +55 -0
  46. data/lib/a2a/push_notification/authentication_info.rb +29 -0
  47. data/lib/a2a/push_notification/config.rb +40 -0
  48. data/lib/a2a/push_notification/dispatcher.rb +52 -0
  49. data/lib/a2a/push_notification/receiver.rb +54 -0
  50. data/lib/a2a/push_notification.rb +11 -0
  51. data/lib/a2a/role.rb +13 -0
  52. data/lib/a2a/security_requirement.rb +19 -0
  53. data/lib/a2a/security_scheme/api_key.rb +33 -0
  54. data/lib/a2a/security_scheme/http_auth.rb +33 -0
  55. data/lib/a2a/security_scheme/mutual_tls.rb +25 -0
  56. data/lib/a2a/security_scheme/oauth2.rb +52 -0
  57. data/lib/a2a/security_scheme/open_id_connect.rb +30 -0
  58. data/lib/a2a/security_scheme.rb +26 -0
  59. data/lib/a2a/streaming/artifact_update_event.rb +40 -0
  60. data/lib/a2a/streaming/response.rb +65 -0
  61. data/lib/a2a/streaming/sse_parser.rb +43 -0
  62. data/lib/a2a/streaming/sse_writer.rb +25 -0
  63. data/lib/a2a/streaming/status_update_event.rb +43 -0
  64. data/lib/a2a/streaming/subscription.rb +56 -0
  65. data/lib/a2a/streaming.rb +12 -0
  66. data/lib/a2a/task/state.rb +31 -0
  67. data/lib/a2a/task/status.rb +35 -0
  68. data/lib/a2a/task.rb +66 -0
  69. data/lib/a2a/version.rb +6 -0
  70. data/lib/a2a/versioning.rb +28 -0
  71. data/lib/a2a.rb +90 -0
  72. 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