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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 447c947dcb0231daa7d00a4c0de158ca737795fcae5f529d71a0ed8d87f9ee3f
4
- data.tar.gz: bc9fa7e1884a169d45857a9febea788630d9917022e8919819357b4e57914d35
3
+ metadata.gz: c92f9dcc050b23c2c9c94ac191bfc71826aa486140f348f19d878733f1c50e0d
4
+ data.tar.gz: 3d628d83f6a58c438702c6bb47fabf4c0f99d0f7feaf4c7188cdd286e522b9f5
5
5
  SHA512:
6
- metadata.gz: 6209ff98b146525320999d3d44e086053e6d767e1a20328436ff34908af222c87912c2e1b78745ce3b646499f1eb33c4e245d4b883fcc169870a1233604e4c10
7
- data.tar.gz: b0b74efa6e45bf7fe84ac63e1d5b2e68cc27386177f1fab728f0ea0582186adb55c7c3c379504b66b05997cc7600cb9e30056143df7311b8af2c591a8b73957d
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.2.0] - 2026-10-08 (stable candidate source; NOT published)
8
-
9
- > **Step 26-4 release preparation only.** Version `0.2.0` has been selected for an exact, source-built candidate. No `v0.2.0` Git tag, GitHub Release, or RubyGems push has been authorized. The published `0.2.0.rc2` pre-release is immutable and **does not include the subsequent fixes** below.
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
- <img src="docs/assets/a2a-rails-handwritten-note.png">
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で広がるAI協働図鑑.png">
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
- > **Release status (2026-10-08):** RubyGems has published **`0.1.0`** and the **`0.2.0.rc2` pre-release**. The current source is being verified as an **unpublished `0.2.0` stable candidate** (Step 26-4). In particular, the Rails/Zeitwerk and migration-generator fixes after rc2 are only in the newer source-built candidate. **No stable `0.2.0` tag, GitHub Release, or RubyGems publication has been authorized.**
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 pre-release: https://github.com/cuichangquan/a2a-rails/releases/tag/v0.2.0-rc.2 (stable `0.2.0` not yet released)
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: [docs/release/v0.1.0-record.md](docs/release/v0.1.0-record.md)
15
- - **Release readiness:** [current release/deployment checklist](docs/release/security-hardening-release-checklist.md) · [v0.1.0 → proposed v0.2 upgrade guide](docs/release/upgrading-v0.1.0-to-v0.2.md). The old rc1 record is historical evidence only.
16
- - **Roadmap / 次にやること:** [ROADMAP.md](ROADMAP.md) — [Step 26 / Issue #62](https://github.com/cuichangquan/a2a-rails/issues/62) tracks the `0.2.0` stable candidate and release gates; [production Issue #11](https://github.com/cuichangquan/a2a-rails/issues/11) remains open. See [stable-readiness record](docs/release/v0.2.0-stable-readiness.md).
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 with the current unreleased `a2a-rails` source pinned by Git commit (the published RubyGems v0.1.0 does not contain direct-Message support). The separate Demo covers Agent Card discovery, JSON-RPC v1.0 SendMessage **Task and direct Message**, GetTask, ListTasks and error handling.
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 real HTTP tests **16/16 checks passed** ([PR #1](https://github.com/cuichangquan/a2a-rails-demo/pull/1)).
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 on unreleased `main`;
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 on unreleased main;
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
- > **Security / production use:** v0.1.0 does not provide built-in authentication or per-caller Task authorization. Do not expose `POST /a2a` to untrusted clients. Before public deployment, protect the endpoint at your application's or network's security boundary. Security hardening is tracked in [Issue #11](https://github.com/cuichangquan/a2a-rails/issues/11).
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 work on main (unreleased; not in published v0.1.0)
289
+ ### Security in v0.2.0 (not in v0.1.0)
274
290
 
275
- The unreleased `main` branch 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).
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; the stable candidate has 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).
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 (merged on main; unreleased)
295
+ ### Step 16-4 request hardening (included in v0.2.0)
280
296
 
281
- The unreleased main branch adds 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).
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 on unreleased `main`; it must match the host verifier. **These improvements are not included in the published v0.1.0 Gem; see the later rc2 pre-release and current stable candidate.**
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
- Unreleased Step 17-3 (not in RubyGems v0.1.0):
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 unreleased.** 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.
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
- **Unreleased 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.
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
- **Unreleased 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.
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
- **Unreleased 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**.
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 — unreleased main source
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