Skip to content

a2a-rust v0.14.1

Choose a tag to compare

@github-actions github-actions released this 30 Sep 16:13
· 977 commits to main since this release

Security

  • PathSegmentTenantResolver let a caller choose its own tenant
    (GHSA-hr9h-6jvf-wvg6).
    The resolver read the :path pseudo-header and,
    when that was absent, an ordinary request header named path. Only the
    WebSocket dispatcher set :path, so on every other entry point the tenant
    came from the path header, which any client can send. A server built
    with .with_tenant_resolver(PathSegmentTenantResolver::new(..)) and served
    over JSON-RPC, HTTP+JSON, gRPC (as a metadata entry) or the Axum adapter's
    A2aRouter ran a request carrying path: /tenants/<other>/x as tenant
    <other>, with that tenant's tasks and push configurations to read, list,
    cancel, subscribe to and write. Path-based tenancy isolates tenants only
    when something in front of the agent authorizes each caller against the
    URL, as the multi-tenancy guide requires of this resolver, and that is the
    control the header defeated: a caller admitted to one tenant's URLs could
    act as another. On JSON-RPC the header overrode the URL outright
    (/tenants/acme with path: /tenants/victim/x ran as victim); on
    HTTP+JSON a URL naming a different tenant was rejected as a mismatch, so
    the header worked on routes without a tenant prefix, such as
    /message:send. Affected: 0.7.0 through 0.14.0, the
    releases in which the handler calls the configured resolver (0.3.0 through
    0.6.0 had the same fallback, but only application code that called the
    resolver itself was exposed). Not affected: the WebSocket binding,
    HeaderTenantResolver, BearerTokenTenantResolver, custom resolvers that
    do not read a path header, and servers with no resolver. The same gap meant the
    legitimate URL form never worked on the HTTP bindings: JSON-RPC served
    /tenants/acme from the default tenant, and HTTP+JSON rejected it with
    400 because the resolver found no tenant to match the URL's.
    • Fix. The JSON-RPC and HTTP+JSON dispatchers now record the request
      URI's path under :path, as the WebSocket dispatcher already did. A real
      header cannot carry that name. The resolver reads :path and nothing
      else.
    • Migration. gRPC and A2aRouter pass the resolver no URL, so there it
      now resolves no tenant: the request is served from the default tenant,
      or rejected under require_resolved_tenant(true). A deployment that used
      this resolver on those entry points was taking the tenant from the client
      all along. Switch it to BearerTokenTenantResolver, or to
      HeaderTenantResolver behind a proxy that sets the header and strips any
      inbound copy.
    • If you cannot upgrade yet, strip the inbound path header (for gRPC,
      the path metadata key, which is the same HTTP/2 header) at a proxy in
      front of the agent, or change resolver as above.
    • Known limitation. The resolver returns the path segment as sent, but
      HTTP+JSON percent-decodes the tenant it takes from the URL, so on that
      binding a tenant whose name needs percent-encoding is rejected with 400.
      This fails closed.
    • Regression tests: crates/a2a-protocol-sdk/tests/tenant_path_resolver_isolation.rs
      sends a forged path header over JSON-RPC, HTTP+JSON, gRPC and
      WebSocket and asserts the executor never ran as that tenant, that the
      URL form selects the tenant where the URL can carry one, and that the
      URL still wins when both are sent;
      crates/a2a-protocol-server/tests/axum_tenant_path_header.rs does the
      same through A2aRouter. Before the fix all but the WebSocket case fail
      with the executor having run as the forged tenant.

Internal

  • The vendored SLIMRPC specification follows upstream's 2026-09-29 merge;
    the binding does not.
    Upstream merged its collaborative-task work to main
    as 1328426, which added three documents to main and changed both
    vendored ones, and the nightly Official TCK run failed on the drift check —
    before the official suite ran, so main went without a full conformance
    result until this landed. All five documents are now vendored. Everything the
    merge added to the two the binding implements is A2A 1.1 (SendLiveMessage,
    a multicast form of it, and the slimrpc-context-map key it uses), and no
    A2A 1.1 specification has been released, so the binding is unchanged; the
    three collaborative-task documents are byte-identical to the branch text
    already triaged as not followed. The reasoning is in
    spec/slimrpc_v1/README.md. The binding's README and book chapter no longer
    say those documents never reached main, and now qualify "complete" as the
    A2A 1.0 surface. The three triage entries they held in
    scripts/check_slimrpc_spec.sh are removed, since a file on main can no
    longer match one.
  • Two a2a-protocol-server unit tests failed intermittently, each for a
    reason in the test.
    Both surfaced on Dependabot pull requests whose diffs
    could not have caused them (#145, #146).
    the_rps_unit_is_multiplied_by_the_window failed on macOS with its 121st
    request admitted: the limiter counts in fixed windows aligned to the wall
    clock, so a minute boundary inside the test's loop resets the count, as it
    should. It and the one-second-window tenant test now discard any attempt
    that straddles a boundary and retry, via refused_within_one_window.
    concurrent_claims_of_one_key_produce_exactly_one_winner failed on Windows
    with no winner at all, which only a pre-existing claim can produce. Its
    database lived at a2a-idem-{pid}, and the removal ran while the store
    held the file open, which Windows refuses; a later test process given the
    same PID found the key already claimed. Reproduced by leaving such a
    database behind: 0 claimed, 16 replayed, the CI signature exactly. It and
    a_migrated_store_can_take_an_artifact_append, which used the same naming,
    now use a per-run directory and drop the store before removing it.
  • prove_workflow_gates_fail.py could not probe a release after a patch
    release.
    Its breaking-patch scenario bumped to {major}.{minor}.1,
    which after 0.14.1 is the version already declared, so the bump was an
    empty commit and the probe crashed the Static checks job on this
    release's own pull request. It now bumps the patch number.