You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.