a2a-rails 0.2.0 → 0.3.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 +73 -3
- data/README.md +45 -25
- data/lib/a2a/rails/client/agent_card_resolver.rb +190 -0
- data/lib/a2a/rails/client/codec.rb +106 -0
- data/lib/a2a/rails/client/outbound_policy.rb +196 -0
- data/lib/a2a/rails/client/pinned_https_transport.rb +227 -0
- data/lib/a2a/rails/client/public_types.rb +58 -0
- data/lib/a2a/rails/client.rb +295 -0
- data/lib/a2a/rails/configuration.rb +6 -2
- data/lib/a2a/rails/engine.rb +4 -1
- data/lib/a2a/rails/version.rb +1 -1
- data/lib/a2a-rails.rb +1 -0
- metadata +13 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c92f9dcc050b23c2c9c94ac191bfc71826aa486140f348f19d878733f1c50e0d
|
|
4
|
+
data.tar.gz: 3d628d83f6a58c438702c6bb47fabf4c0f99d0f7feaf4c7188cdd286e522b9f5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2c7219a18e0488c9ba0bce72fc896c3d963ee76a58e4d1012c63e1f3fbd70b561c97cc479365ac448d570546f8aba6ebc04fd8eda17439c776081cd8085203a5
|
|
7
|
+
data.tar.gz: 3de85240921dd0d61b2236f782f814105b23e96e8c6892d69d050de4e562e0f7fa2068a9fd9c23d98450ad1013848569201817ccd16fd83d0897ec2fdd50cfa0
|
data/CHANGELOG.md
CHANGED
|
@@ -4,9 +4,79 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
-
## [0.
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
## [0.3.0] — Rails Server compatibility and outbound Client
|
|
8
|
+
|
|
9
|
+
`0.3.0` advances the outbound A2A Client tested in `0.3.0.rc1` while retaining the existing Rails A2A Server default. [Release verification record](docs/release/v0.3.0-stable-candidate-record.md) and [Step 31 tracking issue](https://github.com/cuichangquan/a2a-rails/issues/120) contain the dated CI/provenance history. Consult [GitHub Releases](https://github.com/cuichangquan/a2a-rails/releases) or [RubyGems versions](https://rubygems.org/gems/a2a-rails/versions) for actual publication status; source versions and CI artifacts alone are not public releases.
|
|
10
|
+
|
|
11
|
+
### Added and retained behavior
|
|
12
|
+
|
|
13
|
+
- Outbound `A2A::Rails::Client` for secure HTTPS Agent Card discovery and A2A v1.0 JSON-RPC `SendMessage`, `GetTask`, `ListTasks`, and `CancelTask`; immutable Ruby DTOs preserve Task, direct Message, rich Parts, cursors and protocol errors.
|
|
14
|
+
- Client-only Rails hosts can set `config.server_enabled = false`. Existing Server applications keep `server_enabled = true` by default and retain the authenticated Task Store, ActiveRecord and ActiveJob behavior shipped in `0.2.0`.
|
|
15
|
+
- Public outbound Client has exact HTTPS-origin allowlisting, DNS and pinned public-IPv4 socket checks, peer/hostname/SNI certificate verification, Card/RPC credential separation, bounded HTTP and parsing, and no automatic side-effect retries.
|
|
16
|
+
|
|
17
|
+
### Dependency security and compatibility
|
|
18
|
+
|
|
19
|
+
- The direct `json` dependency now requires **`>= 2.19.9, < 3`** after an early `0.3.0` candidate audit caught vulnerable older resolved versions. Ruby 3.3/3.4/4.0 and Rails 8.0/8.1 host compatibility is exercised in CI.
|
|
20
|
+
- A full Rails host can independently resolve an affected `net-imap` release via its mail stack. The six-host candidate audit selects patched `net-imap ~> 0.5.15`; `net-imap` is **not** a direct dependency of this Gem. **Consumers must update/audit their own Gemfile.lock**.
|
|
21
|
+
- The final candidate's six audited host lockfiles had no matching advisories in RubySec at the time of the 2026-10-10 check; this is **not** a guarantee about future advisories, other lockfiles, host Ruby/OS or independently audited security.
|
|
22
|
+
- Existing local test-only TLS adapters are not supported production Client bypasses. No streaming/SSE, push, gRPC, IPv6-only outgoing hosts, generic retries/polling, OAuth issuer or universal production identity/tenant configuration is included.
|
|
23
|
+
|
|
24
|
+
### Documentation and evidence
|
|
25
|
+
|
|
26
|
+
- [Upgrade guide from v0.2.0](docs/release/upgrading-v0.2.0-to-v0.3.md), [RC1 feedback snapshot](docs/release/v0.3.0-rc1-feedback-and-upgrade-readiness.md), [candidate provenance](docs/release/v0.3.0-stable-candidate-record.md) and [confidential security reporting](SECURITY.md).
|
|
27
|
+
- Official pinned A2A TCK baseline remains **63 passed / 1 known upstream-fixture failure / 171 skipped / 30 deselected**; green informational workflow does not establish full A2A certification.
|
|
28
|
+
- Gem publication and exposing a public/no-auth A2A Server are **separate decisions**. Deployment requires host IdP, business authorization, rate limits, durable workers/storage and safe logging per [Issue #11](https://github.com/cuichangquan/a2a-rails/issues/11).
|
|
29
|
+
|
|
30
|
+
## [0.3.0.rc1] - 2026-10-10 (published pre-release)
|
|
31
|
+
|
|
32
|
+
> **Published after explicit owner GO:** [RubyGems RC1](https://rubygems.org/gems/a2a-rails/versions/0.3.0.rc1) and [GitHub Pre-release](https://github.com/cuichangquan/a2a-rails/releases/tag/v0.3.0-rc1), from source `664a201e6df9173e1656bb5f25b503821c0be2b5`, using the same build-once Gem. SHA256: `e5b184e615f9f59c3db770d150b96e847fa5df83ca152e274ae55c656b68feda`. See the [publication record](docs/release/v0.3.0-rc1-publication-record.md). At the time of the `0.3.0.rc1` pre-release announcement, stable `0.3.0` was not yet released.
|
|
33
|
+
|
|
34
|
+
> **Pre-release candidate, not a stable-production security certification.**
|
|
35
|
+
> This candidate contains the previously unreleased outbound Client changes
|
|
36
|
+
> made after published stable `0.2.0`. The candidate source/gemspec VERSION
|
|
37
|
+
> is `0.3.0.rc1`. [Technical release gates](docs/release/step-29-5u-risk-based-oss-release-policy.md)
|
|
38
|
+
> require exact artifact verification, no known unresolved Critical/High
|
|
39
|
+
> vulnerabilities and an explicit owner GO for publication. An external
|
|
40
|
+
> security review is encouraged but **not a mandatory release condition**.
|
|
41
|
+
> Public A2A deployment security is tracked separately in [Issue #11](https://github.com/cuichangquan/a2a-rails/issues/11).
|
|
42
|
+
|
|
43
|
+
### Added — outbound Client capabilities (v0.3.0.rc1)
|
|
44
|
+
|
|
45
|
+
- Outbound Rails `A2A::Rails::Client` for strict HTTPS Agent Card
|
|
46
|
+
discovery and A2A v1.0 JSON-RPC `SendMessage`, `GetTask`, `ListTasks`,
|
|
47
|
+
`CancelTask`, with separate Card/RPC credential callbacks, immutable
|
|
48
|
+
plain-Ruby results, direct Message or Task output and Client-only Rails mode.
|
|
49
|
+
- Explicit approved origin and public-IPv4 pinned socket policy, ordinary
|
|
50
|
+
hostname/SNI/CA verification, zero automatic transport retries, bounded
|
|
51
|
+
timeouts/request-response sizes and safe error categories. Host secrets,
|
|
52
|
+
tenant authorization and recovery are **not** implemented automatically.
|
|
53
|
+
- Structural validation of Message, nested Task/Artifact and File/Data Parts.
|
|
54
|
+
Native official Python and Go SDK local HTTPS interoperability, plus
|
|
55
|
+
genuine Solid Queue/Sidekiq separately executing outbound Client workers
|
|
56
|
+
on Rails 8.0/8.1, all tracked with scoped test limitations.
|
|
57
|
+
|
|
58
|
+
### Security and compatibility notes
|
|
59
|
+
|
|
60
|
+
- Source-hardening tests cover DNS/SSRF, TLS/auth/log privacy, gzip and
|
|
61
|
+
malformed/chunked/truncated HTTP, JSON nesting, ambiguous remote
|
|
62
|
+
SendMessage/CancelTask timeout and nested response validation.
|
|
63
|
+
- **Go SDK follow-up:** Step 29-5q verified authenticated Go ListTasks pagination with two scoped test users and isolated Task visibility; anonymous callers still receive `-31401`.
|
|
64
|
+
- **Release evidence and limitations:** Exact-version source and Gem are
|
|
65
|
+
verified by build-once/SHA256, installed Ruby/Rails/PostgreSQL,
|
|
66
|
+
native Python/Go interoperability and security regression checks; the
|
|
67
|
+
public RubyGems download matches the verified Gem byte-for-byte.
|
|
68
|
+
[Step 29-5u risk-based release policy](docs/release/step-29-5u-risk-based-oss-release-policy.md)
|
|
69
|
+
makes third-party security audit **optional** and does not assert a
|
|
70
|
+
certification. No known unresolved Critical/High code defect is acceptable
|
|
71
|
+
in release scope. Production host security review remains separate.
|
|
72
|
+
- Publishing a Gem and approving open-public **production deployment**
|
|
73
|
+
are distinct; a host must provide credential issuer verification,
|
|
74
|
+
business authorization, rate/concurrency and operational limits.
|
|
75
|
+
No release approval is implied by any CI PASS.
|
|
76
|
+
|
|
77
|
+
## [0.2.0] - 2026-10-08 (published stable release)
|
|
78
|
+
|
|
79
|
+
> **Published in Step 27 after explicit approval.** [GitHub Release](https://github.com/cuichangquan/a2a-rails/releases/tag/v0.2.0) and [RubyGems 0.2.0](https://rubygems.org/gems/a2a-rails/versions/0.2.0) contain the exact verified artifact, with no rebuild. See the [publication record](docs/release/v0.2.0-publication-record.md) for its SHA256 and public-download verification. The immutable `0.2.0.rc2` pre-release **does not include the subsequent fixes** below.
|
|
10
80
|
|
|
11
81
|
### Added
|
|
12
82
|
|
data/README.md
CHANGED
|
@@ -1,19 +1,27 @@
|
|
|
1
1
|
# a2a-rails
|
|
2
|
-
|
|
2
|
+
|
|
3
|
+
> **Versions and release evidence:** This source contains the `0.3.0` code, including the outbound A2A Client. To check whether a specific version has been **published**, consult [RubyGems versions](https://rubygems.org/gems/a2a-rails/versions) and [GitHub Releases](https://github.com/cuichangquan/a2a-rails/releases). The [Step 31 candidate record](docs/release/v0.3.0-stable-candidate-record.md) tracks the prepublication build/hash; successful CI is not permission to expose a production Agent publicly.
|
|
4
|
+
<img src="docs/assets/a2a-rails-infographics.png">
|
|
5
|
+
<br/>
|
|
3
6
|
<br/>
|
|
4
|
-
<img src="docs/assets/a2a-rails
|
|
7
|
+
<img src="docs/assets/a2a-rails-infographics-en.png">
|
|
5
8
|
|
|
6
9
|
Rails-native integration for exposing Rails applications as A2A v1.0 agents.
|
|
7
10
|
|
|
8
|
-
> **
|
|
11
|
+
> **v0.3.0.rc1 prerelease history (2026-10-10):** `0.3.0.rc1` is published on [RubyGems](https://rubygems.org/gems/a2a-rails/versions/0.3.0.rc1) and as a [GitHub Pre-release](https://github.com/cuichangquan/a2a-rails/releases/tag/v0.3.0-rc1). It adds the outbound Rails Client and Client-only mode. Install with `gem install a2a-rails -v 0.3.0.rc1`, or pin `gem "a2a-rails", "0.3.0.rc1"` in your Gemfile. Stable `0.2.0` remains available. RC1 is not stable `0.3.0`, an independent audit, or public-production approval; see the [publication record](docs/release/v0.3.0-rc1-publication-record.md) and [Client guide](docs/guides/outbound-a2a-client.md).
|
|
12
|
+
|
|
13
|
+
> **Release status (2026-10-08):** **`0.2.0` is published as a stable release** on [RubyGems](https://rubygems.org/gems/a2a-rails/versions/0.2.0) and [GitHub](https://github.com/cuichangquan/a2a-rails/releases/tag/v0.2.0). It includes the Rails/Zeitwerk and migration-generator fixes after rc2. The public Gem matches the verified artifact byte-for-byte; see the [publication record](docs/release/v0.2.0-publication-record.md).
|
|
9
14
|
|
|
10
15
|
- RubyGems: https://rubygems.org/gems/a2a-rails
|
|
11
|
-
- Published
|
|
16
|
+
- Published version list: https://rubygems.org/gems/a2a-rails/versions
|
|
17
|
+
- Earlier stable release `0.2.0`: https://github.com/cuichangquan/a2a-rails/releases/tag/v0.2.0
|
|
12
18
|
- Previous stable release: https://github.com/cuichangquan/a2a-rails/releases/tag/v0.1.0
|
|
13
19
|
- Changelog: [CHANGELOG.md](CHANGELOG.md)
|
|
14
|
-
- Release record: [
|
|
15
|
-
- **
|
|
16
|
-
- **
|
|
20
|
+
- Release record: [v0.2.0 publication record](docs/release/v0.2.0-publication-record.md)
|
|
21
|
+
- **Security policy / 脆弱性報告:** [SECURITY.md](SECURITY.md) — confidential vulnerability reporting, current assurance boundaries and production responsibilities.
|
|
22
|
+
- **v0.3.x prerelease policy:** [Risk-based release gates](docs/release/step-29-5u-risk-based-oss-release-policy.md) — independently commissioned external audit is **recommended but optional**, not a known vulnerability or RubyGems requirement. Technical security checks, no known unresolved Critical/High findings, exact Gem provenance and explicit maintainer release approval remain mandatory; [optional community review #113](https://github.com/cuichangquan/a2a-rails/issues/113). **Check the published-version list for the current stable version; package publication and public-production deployment are separate decisions.**
|
|
23
|
+
- **Release evidence & upgrading:** [Step 31 release-readiness record](docs/release/v0.3.0-stable-readiness.md) · [RC1 feedback snapshot](docs/release/v0.3.0-rc1-feedback-and-upgrade-readiness.md) · [v0.2.0 → v0.3.0 upgrade guide](docs/release/upgrading-v0.2.0-to-v0.3.md) · [v0.1.0 → v0.2.0 guide](docs/release/upgrading-v0.1.0-to-v0.2.md). Check [RubyGems](https://rubygems.org/gems/a2a-rails/versions) for availability. Gem distribution does **not** authorize public Agent exposure.
|
|
24
|
+
- **Roadmap / 次にやること:** [ROADMAP.md](ROADMAP.md) · [Step 31 / Issue #120](https://github.com/cuichangquan/a2a-rails/issues/120) for the `0.3.0` release-evaluation and publication decisions. [Issue #11](https://github.com/cuichangquan/a2a-rails/issues/11) still governs public-production deployment; the [historical `0.2.0` release record](docs/release/v0.2.0-stable-readiness.md) is separate.
|
|
17
25
|
- **Official A2A TCK results:** [Pinned JSON-RPC MUST report and reproduction](docs/testing/official-a2a-tck.md) — after Step 17-4: **63 passed / 1 failed / 171 skipped / 30 deselected** (pytest). The remaining `CORE-SEND-003` mismatch is tracked [upstream in #202](https://github.com/a2aproject/a2a-tck/issues/202). The TCK workflow is informational, **not** an A2A conformance certificate.
|
|
18
26
|
|
|
19
27
|
- [A2Aの全体像(日本語・A4 1枚PDF)](docs/guides/a2a-protocol-overview-ja.pdf) — 登場人物・依頼の流れ・主要用語・MCPとの違いをまとめた学習資料。
|
|
@@ -23,12 +31,19 @@ Rails-native integration for exposing Rails applications as A2A v1.0 agents.
|
|
|
23
31
|
|
|
24
32
|
**[a2a-rails-demo — independent Rails 8 Echo Agent](https://github.com/cuichangquan/a2a-rails-demo)**
|
|
25
33
|
|
|
26
|
-
Run a complete standalone Rails Agent
|
|
34
|
+
Run a complete standalone Rails Agent using the **published RubyGems `a2a-rails = 0.2.0`** (not a Git source checkout). The separate Demo covers Agent Card discovery, JSON-RPC v1.0 SendMessage **Task and direct Message**, GetTask, ListTasks and error handling.
|
|
27
35
|
|
|
28
36
|
- [Demo Quick Start, code and example curl requests](https://github.com/cuichangquan/a2a-rails-demo#quick-start).
|
|
29
|
-
- [Demo GitHub Actions smoke](https://github.com/cuichangquan/a2a-rails-demo/actions/workflows/smoke.yml) — independent
|
|
37
|
+
- [Demo GitHub Actions smoke](https://github.com/cuichangquan/a2a-rails-demo/actions/workflows/smoke.yml) — independent Rails 8.1.0 / Ruby 3.4.10 HTTP smoke **16/16 checks passed** against published Gem `0.2.0` ([Step 28 Demo PR #2, merged](https://github.com/cuichangquan/a2a-rails-demo/pull/2), [passing PR CI](https://github.com/cuichangquan/a2a-rails-demo/actions/runs/37736426834)). The CI also confirms `A2A::Rails::VERSION == "0.2.0"`, RubyGems as the dependency source, and the demo's production-startup refusal.
|
|
30
38
|
- **Local-only development/test** demo: no trusted verifier or durable Task store, **not** an internet-facing production template. See [production security](docs/guides/production-security.md).
|
|
31
39
|
|
|
40
|
+
### Outbound Client-only Rails Demo / 外部A2A Agentを呼び出すサンプル
|
|
41
|
+
|
|
42
|
+
**[a2a-rails-client-demo — standalone Rails 8 Client](https://github.com/cuichangquan/a2a-rails-client-demo)**
|
|
43
|
+
|
|
44
|
+
Independent Rails application using **published RubyGems `a2a-rails = 0.3.0.rc1`** with `server_enabled = false`. Demonstrates Agent Card discovery, SendMessage **Task or direct Message**, GetTask and ListTasks with exact HTTPS origin allowlisting. [Client-only CI PASS](https://github.com/cuichangquan/a2a-rails-client-demo/actions/runs/38048228531) and [independent Rails-to-Rails pinned-TLS CI PASS](https://github.com/cuichangquan/a2a-rails-client-demo/actions/runs/38048228488): the published RC1 Client calls an independently installed **published 0.2.0 Server** over real TLS using a **loopback-only test bridge and test-only pinned policy override**, including actual Echo business logic. This is **not** a public-DNS/public-CA, no-injection or production deployment test. The existing Server demo and shipped security policy remain unchanged; see [Step 30 #117](https://github.com/cuichangquan/a2a-rails/issues/117).
|
|
45
|
+
|
|
46
|
+
|
|
32
47
|
## What is a2a-rails?
|
|
33
48
|
|
|
34
49
|
`a2a-rails` lets a Rails application expose an A2A-compatible Agent without making application code depend directly on SDK-specific request and response objects.
|
|
@@ -50,16 +65,17 @@ The Gem provides:
|
|
|
50
65
|
- a Rails-native Agent / Skill DSL;
|
|
51
66
|
- A2A Agent Card generation;
|
|
52
67
|
- automatically mounted A2A HTTP endpoints;
|
|
53
|
-
- synchronous Task execution by default, plus opt-in ActiveJob-backed async Task execution
|
|
68
|
+
- synchronous Task execution by default, plus opt-in ActiveJob-backed async Task execution in published `0.2.0`;
|
|
54
69
|
- SDK-independent Handler inputs;
|
|
55
|
-
- a process-local MemoryStore by default, plus an optional durable ActiveRecordStore
|
|
70
|
+
- a process-local MemoryStore by default, plus an optional durable ActiveRecordStore in `0.2.0`;
|
|
56
71
|
- Rails generators for initial setup;
|
|
57
|
-
- an internal Protocol Adapter boundary around the upstream SDK
|
|
72
|
+
- an internal Protocol Adapter boundary around the upstream SDK;
|
|
73
|
+
- **in 0.3.x**, a Rails outbound `A2A::Rails::Client` with strict HTTPS Agent Card discovery, A2A v1.0 JSON-RPC communication and optional Client-only mode.
|
|
58
74
|
|
|
59
75
|
v0.1 is intentionally **server-first** and **non-streaming**.
|
|
60
76
|
|
|
61
77
|
> [!WARNING]
|
|
62
|
-
> **
|
|
78
|
+
> **Versioned security note:** The original **v0.1.0** did not implement per-caller Task authorization. **0.2.0 and the subsequent 0.3.x line** include a host-owned authentication hook, principal-scoped Task operations and fail-closed non-development/test behavior. **Neither version supplies your identity provider or business authorization, and publication does not approve an anonymously exposed production endpoint.** See [SECURITY.md](SECURITY.md), [authentication guide](docs/guides/authentication.md) and [deployment #11](https://github.com/cuichangquan/a2a-rails/issues/11).
|
|
63
79
|
|
|
64
80
|
## Requirements
|
|
65
81
|
|
|
@@ -270,17 +286,17 @@ POST /a2a
|
|
|
270
286
|
|
|
271
287
|
The Rails Engine is mounted automatically.
|
|
272
288
|
|
|
273
|
-
### Security
|
|
289
|
+
### Security in v0.2.0 (not in v0.1.0)
|
|
274
290
|
|
|
275
|
-
|
|
291
|
+
Published `0.2.0` includes a host-provided `config.authenticate_request` callback for `POST /a2a`, plus per-principal Task ownership checks. Without an authenticator, production and other non-development/test environments fail closed; the local development/test Quick Start remains available. See [Authentication guide](docs/guides/authentication.md).
|
|
276
292
|
|
|
277
|
-
> **Important:** These protections are **absent from RubyGems 0.1.0** but included in the published `0.2.0.rc2` pre-release;
|
|
293
|
+
> **Important:** These protections are **absent from RubyGems 0.1.0** but included in the published `0.2.0.rc2` pre-release; stable `0.2.0` includes additional fixes. Neither pre-release nor stable source automatically makes a deployed endpoint production-secure. Distributed rate limits, business authorization and the deployment review remain tracked in [Issue #11](https://github.com/cuichangquan/a2a-rails/issues/11).
|
|
278
294
|
|
|
279
|
-
### Step 16-4 request hardening (
|
|
295
|
+
### Step 16-4 request hardening (included in v0.2.0)
|
|
280
296
|
|
|
281
|
-
|
|
297
|
+
Published `0.2.0` includes a bounded JSON-RPC body (`config.max_request_bytes`, default 1 MiB), Content-Type validation, stricter parameter checks, a pagination snapshot cap, and reduced exception logging. See [Request Hardening Guide](docs/guides/request-hardening.md).
|
|
282
298
|
|
|
283
|
-
Rate limiting, application-specific authorization and production deployment safeguards remain the host's responsibility or future work. Step 16-5 adds explicit A2A Agent Card Bearer authentication advertisement
|
|
299
|
+
Rate limiting, application-specific authorization and production deployment safeguards remain the host's responsibility or future work. Step 16-5 adds explicit A2A Agent Card Bearer authentication advertisement in published `0.2.0`; it must match the host verifier. **These improvements are not included in the published v0.1.0 Gem; see the published stable `0.2.0` release.**
|
|
284
300
|
|
|
285
301
|
### Production deployment review (Step 16-6)
|
|
286
302
|
|
|
@@ -337,14 +353,14 @@ Hash / Array → Data Part
|
|
|
337
353
|
nil → no Artifact
|
|
338
354
|
other object → ArtifactMappingError
|
|
339
355
|
|
|
340
|
-
|
|
356
|
+
Included since v0.2.0 (not in v0.1.0):
|
|
341
357
|
A2A::Rails::FileArtifact.bytes(...) → File Part with base64 raw bytes
|
|
342
358
|
A2A::Rails::FileArtifact.url(...) → File Part with an HTTPS URL
|
|
343
359
|
```
|
|
344
360
|
|
|
345
|
-
**File Artifact output is
|
|
361
|
+
**File Artifact output is included in v0.2.0.** Rails Handlers can explicitly return a `FileArtifact.bytes(data:, filename:, media_type:)` or `FileArtifact.url(url:, filename:, media_type:)`. See the [File Artifact output guide](docs/guides/file-artifacts.md). Input file Parts, downloading remote URLs and production file authorization are not supplied by the Gem.
|
|
346
362
|
|
|
347
|
-
**
|
|
363
|
+
**Included in v0.2.0 (Step 17-4):** A2A v1.0 also permits a direct `Message` from `SendMessage`. The host Agent can opt into `response_mode :message` or select the mode using a callable; the default remains `:task`. Direct replies do **not** create Task records. See the [Direct Message response guide](docs/guides/direct-message-responses.md) for the contract and safety implications.
|
|
348
364
|
|
|
349
365
|
Supported Task operations:
|
|
350
366
|
|
|
@@ -357,9 +373,9 @@ Supported Task operations:
|
|
|
357
373
|
|
|
358
374
|
The default `Task::MemoryStore` is thread-safe but process-local. Tasks and pagination cursors are not durable across process restarts and are not shared between processes.
|
|
359
375
|
|
|
360
|
-
**
|
|
376
|
+
**Included in v0.2.0 (Step 21):** applications that need durable, multi-worker Task state can opt into `config.task_store = :active_record`. The ActiveRecordStore uses owner-scoped SQL access, row-locked transitions, signed keyset cursors, terminal retention, bounded pruning and maintenance limits. See [ActiveRecord Task Store](docs/guides/active-record-task-store.md). PostgreSQL 16 persistence/locking smoke is verified in CI.
|
|
361
377
|
|
|
362
|
-
**
|
|
378
|
+
**Included in v0.2.0 (Step 22):** Task execution remains synchronous by default. Hosts can opt into ActiveJob-backed async execution globally, per Agent, or per Skill; precedence is **Skill > Agent > global**.
|
|
363
379
|
|
|
364
380
|
```ruby
|
|
365
381
|
A2A::Rails.configure do |config|
|
|
@@ -485,7 +501,7 @@ Artifact: Echo: Hello
|
|
|
485
501
|
|
|
486
502
|
See [docs/release/v0.1.0-record.md](docs/release/v0.1.0-record.md) for the complete release evidence.
|
|
487
503
|
|
|
488
|
-
## Cross-language A2A interoperability —
|
|
504
|
+
## Cross-language A2A interoperability — source verification
|
|
489
505
|
|
|
490
506
|
[Step 18 / PR #27](https://github.com/cuichangquan/a2a-rails/pull/27) verified the **official** Python `a2a-sdk==1.2.2` and Go `a2a-go/v2 v2.6.0` clients against a loopback-only Rails JSON-RPC A2A v1.0 Agent. Both discovered Agent Cards, decoded Task and direct Message responses, queried Tasks and checked terminal cancellation and version errors.
|
|
491
507
|
|
|
@@ -518,6 +534,10 @@ Known warning-enabled output from upstream `agent2agent 2.0.0` can include circu
|
|
|
518
534
|
## Release Documents
|
|
519
535
|
|
|
520
536
|
- [CHANGELOG](CHANGELOG.md)
|
|
537
|
+
- [Current 0.3.0 stable readiness (NOT released)](docs/release/v0.3.0-stable-readiness.md)
|
|
538
|
+
- [Step 31-4 RC1 feedback evidence and risks](docs/release/v0.3.0-rc1-feedback-and-upgrade-readiness.md)
|
|
539
|
+
- [Upgrading from v0.2.0 to planned v0.3.0](docs/release/upgrading-v0.2.0-to-v0.3.md)
|
|
540
|
+
- [RC1 feedback / bug report template](https://github.com/cuichangquan/a2a-rails/issues/new?template=rc1-feedback.md)
|
|
521
541
|
- [v0.1.0 Release Notes](docs/release/v0.1.0.md)
|
|
522
542
|
- [v0.1.0 Release Checklist](docs/release/v0.1.0-checklist.md)
|
|
523
543
|
- [v0.1.0 Release Record](docs/release/v0.1.0-record.md)
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "pinned_https_transport"
|
|
4
|
+
|
|
5
|
+
module A2A
|
|
6
|
+
module Rails
|
|
7
|
+
class Client
|
|
8
|
+
# Internal A2A v1.0 JSON-RPC Agent Card discovery and RPC boundary.
|
|
9
|
+
#
|
|
10
|
+
# NOT a public Client facade: application authentication, DTO contracts,
|
|
11
|
+
# complete schema validation and multi-language HTTP integration follow
|
|
12
|
+
# in later Step 29 PRs. Default construction always uses the secure
|
|
13
|
+
# OutboundPolicy and PinnedHttpsTransport; injection is test-only.
|
|
14
|
+
class AgentCardResolver
|
|
15
|
+
class Error < A2A::Rails::Error
|
|
16
|
+
attr_reader :reason
|
|
17
|
+
|
|
18
|
+
def initialize(reason)
|
|
19
|
+
@reason = reason
|
|
20
|
+
super("Outbound A2A Agent Card rejected: #{reason}")
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
class InvalidCard < Error; end
|
|
25
|
+
class UnsupportedInterface < Error; end
|
|
26
|
+
class InvalidRPC < Error; end
|
|
27
|
+
|
|
28
|
+
class RemoteError < Error
|
|
29
|
+
attr_reader :code
|
|
30
|
+
|
|
31
|
+
def initialize(code)
|
|
32
|
+
@code = code
|
|
33
|
+
super(:remote_json_rpc_error)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
Interface = Struct.new(:url, :tenant, keyword_init: true)
|
|
38
|
+
Discovery = Struct.new(:card, :interface, keyword_init: true)
|
|
39
|
+
|
|
40
|
+
JSONRPC_BINDING = "JSONRPC"
|
|
41
|
+
PROTOCOL_VERSION = "1.0"
|
|
42
|
+
METHODS = %w[SendMessage GetTask ListTasks CancelTask].freeze
|
|
43
|
+
MAX_INTERFACES = 64
|
|
44
|
+
MAX_SKILLS = 256
|
|
45
|
+
|
|
46
|
+
def initialize(agent_card_url:, allowed_origins: nil,
|
|
47
|
+
card_authorization: nil, card_credential_origin: nil,
|
|
48
|
+
authorization: nil, credential_origin: nil,
|
|
49
|
+
policy: nil, transport: nil)
|
|
50
|
+
# Production callers must construct through the forthcoming public
|
|
51
|
+
# Client facade, which will not expose policy/transport overrides.
|
|
52
|
+
@policy = policy || OutboundPolicy.new(allowed_origins: allowed_origins)
|
|
53
|
+
@transport = transport || PinnedHttpsTransport.new(policy: @policy)
|
|
54
|
+
@agent_card_url = agent_card_url
|
|
55
|
+
@card_authorization = card_authorization
|
|
56
|
+
@card_credential_origin = card_credential_origin
|
|
57
|
+
@authorization = authorization
|
|
58
|
+
@credential_origin = credential_origin
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Re-fetch on each invocation; never share card/identity cache.
|
|
62
|
+
# The transport separately resolves and pins the card destination.
|
|
63
|
+
def discover
|
|
64
|
+
response = @transport.get_json(
|
|
65
|
+
url: @agent_card_url,
|
|
66
|
+
authorization: @card_authorization,
|
|
67
|
+
credential_origin: @card_credential_origin
|
|
68
|
+
)
|
|
69
|
+
card = response.json
|
|
70
|
+
validate_card!(card)
|
|
71
|
+
interface = choose_interface!(card.fetch("supportedInterfaces"))
|
|
72
|
+
Discovery.new(card: deep_freeze(card), interface: interface).freeze
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# A thin INTERNAL wire operation, not the public SendMessage API.
|
|
76
|
+
# The supplied params use JSON/protocol camelCase, not Ruby DTO keys.
|
|
77
|
+
def rpc(method:, params:, id:)
|
|
78
|
+
unless METHODS.include?(method) && params.is_a?(Hash) &&
|
|
79
|
+
params.keys.all? { |key| key.is_a?(String) } &&
|
|
80
|
+
!params.key?("tenant") &&
|
|
81
|
+
(id.is_a?(String) && !id.empty? || id.is_a?(Integer))
|
|
82
|
+
raise InvalidRPC, :invalid_parameters
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
interface = discover.interface
|
|
86
|
+
request_params = params.dup
|
|
87
|
+
# A2A 1.0 puts the opaque tenant in the request's params, not the
|
|
88
|
+
# JSON-RPC envelope. The host is forbidden from overriding it.
|
|
89
|
+
request_params["tenant"] = interface.tenant unless interface.tenant.nil?
|
|
90
|
+
wire = {
|
|
91
|
+
"jsonrpc" => "2.0",
|
|
92
|
+
"id" => id,
|
|
93
|
+
"method" => method,
|
|
94
|
+
"params" => request_params
|
|
95
|
+
}
|
|
96
|
+
response = @transport.post_json(
|
|
97
|
+
url: interface.url, json: wire,
|
|
98
|
+
authorization: @authorization,
|
|
99
|
+
credential_origin: @credential_origin
|
|
100
|
+
)
|
|
101
|
+
envelope = response.json
|
|
102
|
+
unless envelope.is_a?(Hash) && envelope["jsonrpc"] == "2.0" &&
|
|
103
|
+
envelope["id"] == id &&
|
|
104
|
+
(envelope.key?("result") ^ envelope.key?("error"))
|
|
105
|
+
raise InvalidRPC, :invalid_envelope
|
|
106
|
+
end
|
|
107
|
+
if envelope.key?("error")
|
|
108
|
+
error = envelope["error"]
|
|
109
|
+
raise InvalidRPC, :invalid_envelope unless error.is_a?(Hash) && error["code"].is_a?(Integer)
|
|
110
|
+
|
|
111
|
+
# The remote error.message/data are untrusted, and may contain
|
|
112
|
+
# secrets. Never reflect them in exception messages or logs.
|
|
113
|
+
raise RemoteError, error.fetch("code")
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
raise InvalidRPC, :invalid_result unless envelope["result"].is_a?(Hash)
|
|
117
|
+
|
|
118
|
+
deep_freeze(envelope.fetch("result"))
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
private
|
|
122
|
+
|
|
123
|
+
def validate_card!(card)
|
|
124
|
+
unless card.is_a?(Hash) &&
|
|
125
|
+
%w[name description version].all? { |key| present_text?(card[key]) } &&
|
|
126
|
+
card["capabilities"].is_a?(Hash) &&
|
|
127
|
+
valid_mode_list?(card["defaultInputModes"]) &&
|
|
128
|
+
valid_mode_list?(card["defaultOutputModes"]) &&
|
|
129
|
+
card["skills"].is_a?(Array) && !card["skills"].empty? &&
|
|
130
|
+
card["skills"].size <= MAX_SKILLS &&
|
|
131
|
+
card["skills"].all? { |s| s.is_a?(Hash) && present_text?(s["id"]) } &&
|
|
132
|
+
card["supportedInterfaces"].is_a?(Array) &&
|
|
133
|
+
!card["supportedInterfaces"].empty? &&
|
|
134
|
+
card["supportedInterfaces"].size <= MAX_INTERFACES
|
|
135
|
+
raise InvalidCard, :invalid_card_shape
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
card.fetch("supportedInterfaces").each do |item|
|
|
139
|
+
unless item.is_a?(Hash) &&
|
|
140
|
+
present_text?(item["url"]) &&
|
|
141
|
+
present_text?(item["protocolBinding"]) &&
|
|
142
|
+
present_text?(item["protocolVersion"])
|
|
143
|
+
raise InvalidCard, :invalid_interface_shape
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def choose_interface!(interfaces)
|
|
149
|
+
candidate = interfaces.find do |entry|
|
|
150
|
+
entry["protocolBinding"] == JSONRPC_BINDING &&
|
|
151
|
+
entry["protocolVersion"] == PROTOCOL_VERSION
|
|
152
|
+
end
|
|
153
|
+
raise UnsupportedInterface, :no_jsonrpc_v1_interface unless candidate
|
|
154
|
+
|
|
155
|
+
if candidate.key?("tenant") &&
|
|
156
|
+
(!candidate["tenant"].is_a?(String) || candidate["tenant"].bytesize > 512)
|
|
157
|
+
raise InvalidCard, :invalid_tenant
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Validate the UNTRUSTED card's selected endpoint independently of
|
|
161
|
+
# card origin; no guessed path, origin fallback or automatic retry.
|
|
162
|
+
target = @policy.resolve!(candidate.fetch("url"))
|
|
163
|
+
Interface.new(url: target.url, tenant: candidate["tenant"]&.dup&.freeze).freeze
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def valid_mode_list?(value)
|
|
167
|
+
value.is_a?(Array) && !value.empty? &&
|
|
168
|
+
value.all? { |element| present_text?(element) }
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def present_text?(value)
|
|
172
|
+
value.is_a?(String) && !value.strip.empty?
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def deep_freeze(value)
|
|
176
|
+
case value
|
|
177
|
+
when Hash
|
|
178
|
+
value.each do |key, element|
|
|
179
|
+
deep_freeze(key)
|
|
180
|
+
deep_freeze(element)
|
|
181
|
+
end
|
|
182
|
+
when Array
|
|
183
|
+
value.each { |element| deep_freeze(element) }
|
|
184
|
+
end
|
|
185
|
+
value.freeze
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module A2A
|
|
4
|
+
module Rails
|
|
5
|
+
class Client
|
|
6
|
+
# Explicit A2A protocol keys only. Arbitrary JSON metadata/data and
|
|
7
|
+
# unknown extension fields are opaque and must NOT have keys rewritten.
|
|
8
|
+
module Codec
|
|
9
|
+
KEYS = {
|
|
10
|
+
"message_id" => "messageId",
|
|
11
|
+
"context_id" => "contextId",
|
|
12
|
+
"task_id" => "taskId",
|
|
13
|
+
"artifact_id" => "artifactId",
|
|
14
|
+
"media_type" => "mediaType",
|
|
15
|
+
"mime_type" => "mimeType",
|
|
16
|
+
"file_name" => "fileName",
|
|
17
|
+
"supported_interfaces" => "supportedInterfaces",
|
|
18
|
+
"protocol_binding" => "protocolBinding",
|
|
19
|
+
"protocol_version" => "protocolVersion",
|
|
20
|
+
"default_input_modes" => "defaultInputModes",
|
|
21
|
+
"default_output_modes" => "defaultOutputModes",
|
|
22
|
+
"input_modes" => "inputModes",
|
|
23
|
+
"output_modes" => "outputModes",
|
|
24
|
+
"accepted_output_modes" => "acceptedOutputModes",
|
|
25
|
+
"return_immediately" => "returnImmediately",
|
|
26
|
+
"history_length" => "historyLength",
|
|
27
|
+
"page_size" => "pageSize",
|
|
28
|
+
"page_token" => "pageToken",
|
|
29
|
+
"next_page_token" => "nextPageToken",
|
|
30
|
+
"total_size" => "totalSize",
|
|
31
|
+
"status_timestamp_after" => "statusTimestampAfter",
|
|
32
|
+
"include_artifacts" => "includeArtifacts",
|
|
33
|
+
"security_schemes" => "securitySchemes",
|
|
34
|
+
"security_requirements" => "securityRequirements",
|
|
35
|
+
"extended_agent_card" => "extendedAgentCard",
|
|
36
|
+
"push_notifications" => "pushNotifications"
|
|
37
|
+
}.freeze
|
|
38
|
+
REVERSE = KEYS.invert.freeze
|
|
39
|
+
OPAQUE = %w[data metadata extensions securitySchemes securityRequirements].freeze
|
|
40
|
+
|
|
41
|
+
module_function
|
|
42
|
+
|
|
43
|
+
def encode(value, opaque: false)
|
|
44
|
+
case value
|
|
45
|
+
when Hash
|
|
46
|
+
convert_hash(value, :encode, opaque: opaque)
|
|
47
|
+
when Array
|
|
48
|
+
value.map { |item| encode(item, opaque: opaque) }
|
|
49
|
+
else
|
|
50
|
+
value
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def decode(value, opaque: false)
|
|
55
|
+
case value
|
|
56
|
+
when Hash
|
|
57
|
+
convert_hash(value, :decode, opaque: opaque)
|
|
58
|
+
when Array
|
|
59
|
+
value.map { |item| decode(item, opaque: opaque) }.freeze
|
|
60
|
+
when String
|
|
61
|
+
value.dup.freeze
|
|
62
|
+
else
|
|
63
|
+
value
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def convert_hash(value, direction, opaque:)
|
|
68
|
+
result = {}
|
|
69
|
+
value.each do |raw_key, entry|
|
|
70
|
+
unless raw_key.is_a?(String) || raw_key.is_a?(Symbol)
|
|
71
|
+
raise InvalidInputError, :invalid_hash_key
|
|
72
|
+
end
|
|
73
|
+
key = raw_key.to_s
|
|
74
|
+
if direction == :encode
|
|
75
|
+
output = opaque ? key : KEYS.fetch(key, key)
|
|
76
|
+
else
|
|
77
|
+
output = !opaque && REVERSE.key?(key) ? REVERSE.fetch(key).to_sym :
|
|
78
|
+
(!opaque && KNOWN_KEYS.include?(key) ? key.to_sym : key)
|
|
79
|
+
end
|
|
80
|
+
raise InvalidInputError, :ambiguous_protocol_key if result.key?(output)
|
|
81
|
+
child_opaque = opaque || OPAQUE.include?(key) || OPAQUE.include?(output.to_s)
|
|
82
|
+
result[output] = if direction == :encode
|
|
83
|
+
encode(entry, opaque: child_opaque)
|
|
84
|
+
else
|
|
85
|
+
decode(entry, opaque: child_opaque)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
direction == :decode ? result.freeze : result
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
KNOWN_KEYS = %w[
|
|
92
|
+
id name description version skills capabilities streaming
|
|
93
|
+
role parts text raw url data filename mimeType mediaType
|
|
94
|
+
status state timestamp artifacts history message task tasks extensions
|
|
95
|
+
tenant contextId messageId taskId artifactId
|
|
96
|
+
supportedInterfaces protocolBinding protocolVersion
|
|
97
|
+
defaultInputModes defaultOutputModes inputModes outputModes
|
|
98
|
+
acceptedOutputModes returnImmediately historyLength
|
|
99
|
+
pageSize pageToken nextPageToken totalSize
|
|
100
|
+
statusTimestampAfter includeArtifacts metadata
|
|
101
|
+
securitySchemes securityRequirements
|
|
102
|
+
].freeze
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|