Repository navigation
These are the sections to read before upgrading. The full notes for this release are in CHANGELOG.md at this tag.
Known Limitations
- Several replicas sharing a store need session affinity. Without every
turn of a task routed to one replica, two replicas can run the same task
at once (the_single_writer_refusal_does_not_cross_replicas), and a
continuation saved on one can overwrite what the other's turn wrote
meanwhile: the stored task loses it, though a subscriber still receives
the event (tests/stale_write_position). Compare-and-set task writes and
cross-replica single-writer admission would close it; they are planned,
not built. One replica, or several with affinity, is the supported
topology for 0.15 (Running More Than One Replica).
Breaking Changes
-
serve::Serveris no longerUnwindSafeorRefUnwindSafewith
tls-rustls(server; on by default in the SDK). The field holding
Server::with_tls's configuration carries rustls' server configuration,
which implements neither. Code that sent aServeracross
std::panic::catch_unwindwraps it inAssertUnwindSafe. Found by
cargo-semver-checks0.51.0 against 0.14.1 (auto_trait_impl_removed),
the one break that run reported which this list did not name. -
The client refuses an agent's object that a2a.proto does not allow
(ClientError::InvalidAgentResponse). Spec §5.7: implementations SHOULD
reject messages missing a REQUIRED field. On every binding (JSON-RPC,
HTTP+JSON, WebSocket, gRPC) the tasksget_task/cancel_task/list_tasks
return, whatsend_messagereturns, every streamed event, and the push
configs the push-config calls return are checked against
a2a_protocol_types::required: a task with an emptyid, a status in
TASK_STATE_UNSPECIFIED, a message with nomessageId/role/parts, an
artifact with noartifactIdor parts, a status/artifact update with no
taskId/contextId, a push config with nourlorauthentication.scheme
now fail the call with the newClientError::InvalidAgentResponse(String)
naming the call and field (not retryable; converts toErrorCode::InvalidAgentResponse).
A refused stream event ends the stream. They used to be returned as if valid.
What integrators see: calls to a non-conformant agent that used to succeed
now fail; matchInvalidAgentResponseto tell this from a transport fault.
ClientErroris#[non_exhaustive], so adding the variant breaks no match.
Crates:a2a-protocol-client,a2a-protocol-types.
Tests:crates/a2a-protocol-client/tests/received_validation/(json.rs,
grpc.rs,cards.rs). -
Agent cards a client receives must pass
AgentCard::validateand
validate_security_schemes.resolve_agent_card,fetch_card_from_url,
CachingCardResolverandget_extended_agent_cardrefuse, with
InvalidAgentResponse, a card the server would refuse to publish (no
description, no skills, emptyprovider.url, a skill with no name, a
signature with nosignature, an emptyprotocolVersion, a security
scheme with an empty REQUIRED string, ...). A refused extended card is not
adopted (its capabilities and security are not acted on).
ClientBuilder::from_carddoes not validate the card it is handed.
Crates:a2a-protocol-client. Tests:
cards.rs a_discovered_card_that_breaks_a_publishing_rule_is_refused,
an_extended_card_that_breaks_a_publishing_rule_is_refused_and_not_adopted. -
The gRPC conversion refuses an absent REQUIRED field.
TryFrom<pb::…>
now returnsConvertError { reason: "missing required field" }for
Task.id,TaskStatus.state(0),Message.message_id/role(0),
Artifact.artifact_id,TaskStatusUpdateEvent/TaskArtifactUpdateEvent
task_id/context_id,AgentCard.name/description/version/
capabilities(no longer defaulted to the empty set),AgentInterface
url/protocol_binding/protocol_version,AgentProvider,AgentSkill
id/name/description,AgentCardSignaturefields,apiKey.name,
httpAuth.scheme,openIdConnectUrland the authorization-code,
client-credentials and device-code flows' URLs — the cases where the same
JSON without the key does not parse. A server answers such a gRPC request
INVALID_ARGUMENTwith aBadRequestviolation (it used to reach the
handler, which refused most of them with the same code). A client reports a
response its conversion refuses asInvalidAgentResponse(it was
Transport). Crates:a2a-protocol-types,a2a-protocol-client.
Tests:crates/a2a-protocol-types/src/proto/convert/required_tests.rs,
card.rs agent_card_missing_capabilities_is_refused,
tests/proptest_proto_convert.rs unspecified_state_and_role_are_refused. -
An authenticating interceptor needs a scheme declared in the public
card, and an extended-card scheme must also be in the public card
(server; spec §13.3). §13.3: agents "MUST authenticate requests using one
of the schemes declared in the publicAgentCard.securitySchemes".
RequestHandlerBuilder::buildcompared the interceptors with the schemes
of the public and extended cards pooled, and passed when none were
declared, so a public card declaring nothing built behind any
authenticator, and a scheme declared only on the extended card passed for
the public card's. It now refuses, withInvalidParamsnaming §13.3:
(1) any authenticating interceptor (bundled or custom) behind a public
card that declares nosecuritySchemes; (2) a scheme the extended card
declares that the public card does not declare under the same name with
the same definition; (3) as before, an authenticator whose
checks_schemeanswersSome(false)to every scheme, now of the public
card only. A custom authenticator that answersNone(the trait default)
is still trusted to check one of the declared schemes: only its author
knows what it checks, and refusing it would refuse every custom
authenticator ever written. Without a public card (without_agent_card)
the extended card's schemes are checked as before.
Migration: declare the scheme your interceptor checks in the card you
pass towith_agent_card: forBearerTokenAuthInterceptoror
JwtAuthInterceptor,securitySchemes: {"bearer": {"httpAuthSecurityScheme": {"scheme": "Bearer"}}}(in Rust,
.with_security_schemes(HashMap::from([("bearer".into(), SecurityScheme::Http(HttpAuthSecurityScheme { scheme: "Bearer".into(), description: None, bearer_format: None }))]))); for
ApiKeyAuthInterceptor, anapiKeySecuritySchemenaming its header; for
MutualTlsAuthInterceptor, amtlsSecurityScheme; and repeat every
extended-card scheme in the public card. The ITK agent now declares its
bearer scheme in every pass (it guards the extended card in every pass),
and the SLIMRPC binding's tests declaremtlsSecuritySchemefor
SlimPeerAuthInterceptor::required(). Tests:
auth::declared_tests::an_authenticator_behind_a_public_card_declaring_no_scheme_is_refused,
auth::declared_tests::a_scheme_declared_only_on_the_extended_card_is_refused. -
The MSRV is 1.94, and the SQL stores use
sqlx0.9 (server, SDK,
SLIMRPC binding; 2026-10-23).sqlx0.9 needs Rust 1.94, and its pool
types are in the SQL stores' public constructors (SqliteTaskStore,
PostgresTaskStoreand the rest take aSqlitePool/PgPool), so an
integrator building one from its own pool moves tosqlx0.9 with this
release.STABILITY.md§5 now lets a minor release raise the MSRV to
adopt a direct dependency's major version, which is this case; it was
folded into 0.15.0 so that the stores' move does not need a second
breaking release. sqlx 0.9 refuses a statement built at run time unless
it is vouched for; the seventeen such statements in the library (the
ListTasksfilters, retention, caller-key erasure, the audit key lookup,
the SQLite schema helpers and the several-part artifact append; counted
withgrep -rn AssertSqlSafeoutside tests) are built from static text,
&'static strnames and integer positions only, and each is marked
sqlx::AssertSqlSafewith that reason.pg_schema::applyand the SQLite
add_applied_seqhelper now take&'static str, so a run-time string
cannot reach them. -
Every agent card declares the budget extension (server; ADR 0023).
https://a2a-rust.com/extensions/budget/v1joins the failure taxonomy
as an extension the builder adds to every card, never as required. A
card that carries a signature but does not declare it is refused at
build(), as for any extension the builder would have to add to signed
bytes. Migration: declare it on the card, then sign the result. -
TenantLimits::max_stored_tasksis removed (server), with
TenantLimits::with_max_stored_tasksand
TenantLimitsBuilder::max_stored_tasks. Deprecated since 0.10.0 and
0.12.0 as "never enforced": the field sat onPerTenantConfig, which a
store never sees, so nothing read it. Migration: give the tenant its own
TaskStoreConfig::max_capacitywith
TenantAwareInMemoryTaskStore::with_tenant_override. The deprecations
made in 0.15.0 stay until 0.16. -
TaskStore::save_appending_historytakes aHistoryLimit, not a
message count (server; release audit F2). The limit bounds bytes as well
as messages. A store that overrides the method trims with
limit.trim(&mut history), and a caller that passednpasses
HistoryLimit::new(n, usize::MAX)to keep the old meaning. -
OpenTelemetry instrumentation is on by default; export is
otlp
(server, client, SDK; decided 2026-10-14, the exporter's place settled
2026-10-18). The server'sotelfeature is now the instrumentation only
(OtelMetrics, the spans' OpenTelemetry bridge), through the OpenTelemetry
API (opentelemetry,tracing-opentelemetry), and is a default feature
of the server, the client and the SDK: a process that installs a provider
gets the SDK's spans and metrics with nothing to enable, and one that
installs none records to nothing.Telemetry, theLangfusepreset and
init_otlp_pipelinemoved to a newotlpfeature, with the SDK and
exporter dependencies; it is default ina2a-protocol-sdkonly.
Migration: a direct dependency ona2a-protocol-serverthat used
Telemetryasks forfeatures = ["otlp"]; on the SDK nothing changes. -
The metric catalogue follows the OpenTelemetry conventions (server,
otel; decided 2026-10-15).a2a.server.latency, deprecated in 0.14.0, is
removed: reada2a.server.request.duration(Prometheus
a2a_server_request_duration_seconds), the same histogram under the
conventions' name, orrpc.server.call.duration. Units are UCUM: a count
is brace-annotated ({request},{connection},{error},{queue},
{delivery},{response}). Rendered throughopentelemetry-prometheus
0.33.1 the other metric names are unchanged (measured 2026-10-18). -
init_otlp_pipelineandinit_otlp_pipeline_with_endpointtake
service_name: Option<&str>(server,otel). The environment now wins:
OTEL_SERVICE_NAME, orservice.nameinOTEL_RESOURCE_ATTRIBUTES, names
the service, and the argument only when they do not;Noneleaves it to
the environment and the SDK'sunknown_servicedefault. The&str
overwrote the environment, against the OpenTelemetry environment-variable
specification. Migration: wrap the name inSome. Tests:
otel::pipeline::tests. -
ConvertErrorandSseParseErrorare#[non_exhaustive](types,
client; release audit F26). A match onSseParseErrorneeds a wildcard
arm, and aConvertErroris built with the now-public
ConvertError::new(field, reason)rather than a struct literal, so a
variant or field added later is not a breaking change. -
A push config whose
authenticationcarries no credentials is
refused (server; spec §4.3.3: a delivery MUST carry the credentials its
config specifies).CreateTaskPushNotificationConfiganswers
InvalidParamsnamingauthentication.credentialswhen they are absent,
empty or whitespace; such a config was stored and every delivery went out
with noAuthorization, or with an emptyBearer. Leave
authenticationout for an unauthenticated webhook. Test:
push_config_credentials. -
A card carrying a credential in a URL is refused when published
(server; spec §14 "The Agent Card SHOULD NOT include sensitive
credentials", §13.3 an extended card no "unmasked credentials").build(),
the static, dynamic and hot-reload card handlers and per-caller extended
cards refuse a card any of whose URLs carries user information
(user:pass@host) or a query parameter named like a secret (token,
access_token,api_key,apikey,auth,client_secret,key,
password,secret,sig,signature; case-insensitive), naming the field. New
publicagent_card::card_credentials(&card)lists them. Tests:
file_url_ssrf::build_refuses_a_card_carrying_a_credential,
agent_card::lint::tests::a_credential_in_any_card_url_is_found_and_refused. -
A card whose OAuth or OpenID Connect URLs are not URLs is refused when
published (types/server; a2a.proto: each OAuth flow URL "MUST be in the
form of a URL", andoauth2_metadata_url"TLS is required").
validate_security_schemes, whichbuild()and every card handler run,
now refuses a flow URL, refresh URL oropenIdConnectUrlthat is not
scheme://authority..., and anoauth2MetadataUrlthat is nothttps.
The deprecated implicit and password flows' URLs are checked only when
given. A client applies the same check to a card it fetches. Tests:
security::url_tests. -
A client built from a card refuses to build when the card marks an
extensionrequiredthat it does not ask for (client; a2a.proto
AgentExtension.required: "the client must understand and comply").
build()returnsExtensionSupportRequirednaming the extension; ask for
it withwith_extensions, or callskip_required_extension_check()when
an interceptor sends theA2A-Extensionsheader. A client built for an
endpoint, not a card, is unaffected. -
build()refuses a card whose declared authentication the
interceptors do not enforce (server; spec §3.3.2 "Servers MUST reject
requests with invalid or missing authentication credentials", §7.4 "MUST
authenticate every incoming request based on ... its declared
authentication requirements", §13.3 "MUST authenticate requests using one
of the schemes declared in the public AgentCard"). Two configurations that
built now fail withServerError::InvalidParams, naming the fix: a card
(or a skill) withsecurityRequirementswhen no interceptor authenticates,
which served every request without the credential the card demanded; and
an authenticating interceptor that checks none of the card's declared
schemes.RequestHandlerBuilder::authenticated_upstream()accepts the
first when a gateway in front of the process authenticates. A requirement
list containing an empty requirement ({}, optional authentication) asks
for nothing. New provided trait method
ServerInterceptor::checks_scheme(&SecurityScheme) -> Option<bool>
(defaultNone, not checked); the bundled authenticators answer it.
Tests:auth::declared_tests, andtests/auth_every_method.rs, which
calls every method inMethod::ALLon both HTTP bindings without a
credential and with a wrong one and requires401each time. -
A replay longer than
subscribe_replay_limitis read in pages
(server; spec §3.5.2 "each stream MUST receive the same events in the
same order"). The limit now bounds each read of the event log, not the
replay. A stream that lagged by more than the limit ended with
streamLagged; it now reads the log a page at a time, from where each
page ended, before it reads the live channel again. The same holds for a
turn aSubscribeToTaskstream missed between polls, and for a
Last-Event-IDresume, which had a silent gap: the replay stopped at the
limit and the stream went on live, past the positions between the
replay's end and where its receiver attached, with nostreamLagged
(event_log_testspinned[snapshot, 1, 2]for a log of five); a
resume whose first read the log had not caught up with within
subscribe_replay_catchupskipped the same way, and now reads on while
the log moves forward (found by an independent review). A page the log
cannot supply is still reported asstreamLagged, after what it did
supply, once, and the final state of a finished task follows the last
page. Memory stays bounded by the limit, but a client resuming with
Last-Event-ID: 0now receives the whole retained history on one
connection, read a page at a time as it is delivered, where it received
at most one page before. -
HTTP+JSON responds
application/a2a+jsonwhen the request states no
preference (server; spec §11.1 "Content-Type: application/a2a+json SHOULD
be used for requests and responses").RestDispatcherand the axum router
answeredapplication/jsonunlessAcceptpreferred the A2A type or the
body was labelled with it. NowAcceptstill decides when it names either
JSON type, anapplication/a2a+jsonbody is still answered in kind, a
request asking only fortext/event-streamgetsapplication/json
(released a2a-go v2.6.0 sends exactly that on its streaming calls and
decodes an error body only underapplication/json), and everything else,
including a request with noAccept, getsapplication/a2a+json.
scripts/go_sdk_interop.shpasses both directions on every binding. A
report asking a2a-go to readapplication/a2a+jsonerrors is drafted,
held, indocs/upstream/. -
A
SubscribeToTaskstream has no idle bound by default (server;
spec §3.1.6 "The stream MUST terminate when the task reaches a terminal
state").HandlerLimits::subscribe_max_idleisOption<Duration>,
Noneby default; it was aDurationof five minutes, after which a
stream to a task still parked ininput-requiredended with no terminal
state.with_subscribe_max_idlesetsSomeand keeps its signature.
Every binding already ends a stream whose client has gone (SSE keep-alive,
the WebSocket read loop, gRPC's closed channel, SLIMRPC's session close),
so the bound is needed only to reclaim subscriptions whose clients are
still connected. A bound too large to add to the clock (Duration::MAX)
panicked inside the stream's poll; it is now no bound. A store that keeps
failing while a stream waits is warned about once per run of failures,
not at every poll (see the upgrading guide). -
SLIMRPC: the binding is built on
agntcy-slim-rpc3.4.1
(a2a-protocol-slimrpc; audit register R13). Its six SLIM requirements
move to the line 3.x requires:agntcy-slim-rpc2.3 → 3.4,
agntcy-slim-auth0.15 → 0.17,agntcy-slim-datapath0.18 → 0.19,
agntcy-slim-service0.12 → 0.13,agntcy-slim-config0.16 → 0.17,
agntcy-slim-session0.7 (locked 0.7.10 → 0.7.21). They are public
dependencies:SlimRpcServer::from_app,SlimRpcTransport::from_app,
SlimRpcMulticast::from_appand the builders'with_identitytake SLIM
types, so a caller passes types from the same SLIM versions. The binding's
own code compiled unchanged. Over the wire, SLIM 3.x sessions are MLS and
verify the peer's identity token on the request that sets one up; a peer
on the 2.x line was not tried against it. -
TaskState's discriminants are its proto numbers (types; spec §1.4,
ADR 0016).TaskStateis generated froma2a.protonow, and
TaskState::Completed as i32is 3, asTASK_STATE_COMPLETEDis in the
proto and the prost enum; it was 5, its place in the hand-written
declaration. The wire form andTaskState::ALLare unchanged. -
ClientRequesthas a new public field,query_params(client;
a2a.protoAPIKeySecurityScheme.location: "Valid values are "query",
"header", or "cookie""; audit register R67). The client refused an API
key that goes in the query string, because a request reached the
transports with headers only. An interceptor now adds query parameters
toquery_params, and the JSON-RPC and HTTP+JSON transports append them
to the request URL, percent-encoded, after any query the route builds
itself; a retried call sends them on every attempt.Debugprints their
names, never their values. A struct literal that names every field of
ClientRequestno longer compiles: build it withClientRequest::new,
or addquery_params: Vec::new(). This field is why the next release
is 0.15.0 rather than 0.14.2.The
Transporttrait gainssend_request_with_queryand
send_streaming_request_with_query, with defaults, so a custom
transport still compiles. The default sends a call with no query
parameters as before and refuses one with them, with
ClientError::InvalidAuthScheme, rather than send it without its
credential. gRPC, WebSocket and the SLIMRPC binding have no query string
and refuse the same way. -
A caller is named by how it authenticated (server; spec §13.1,
RFC 7519 §4.1.2; audit register R12). The bundled interceptors record
bearer:<label>,api-key:<label>andjwt:<issuer>:<sub>(the issuer
percent-encoded) where they recorded the bare label orsub. Asubis
unique only within its issuer, so onesubfrom two accepted issuers was
one caller: one task partition, one rate-limit budget, one approver. The
new identity is what caller isolation, the rate limiter, the audit actor,
ApprovalRequest.requested_byandApprovalGate::with_approverssee.
An approver allowlist of bare names matches nobody: write it with
auth::caller_id, e.g.caller_id::bearer("alice"). Code that compares
CallContext::caller_identity()against a label must compare against the
qualified form. Stored partitions keep their old keys until migrated:
runmigrate_caller_keysonce on each SQLite or PostgreSQL tenant-aware
task and push-config store (see Added), or each caller's tasks and
webhooks are not found under its new identity. A pending approval stored
before the upgrade names its requester bare; the four-eyes check still
refuses that requester, under any issuer. -
A JWT with no
subis refused under caller isolation (server; audit
register R12). It authenticated and named nobody, and under the default
isolation was served the tenant's shared records. It is now refused with
PermissionDeniedunless callers share tasks
(share_tasks_across_callers).
Deprecated
-
signing::sign_agent_card(types; spec §8.4.2), for
sign_card_es256(card, key, key_id), whosekey_idis a&str. Its
key_id: Option<&str>let a signature's protected header omitkid,
which §8.4.2 requires, so it now refusesNonewithInvalidParams
instead of writing a header without one: a specification correction
(STABILITY.md§2), which is the only change for a caller passing a key
id. Kept for the one minor releaseSTABILITY.md§3 requires and removed
in 0.16.0. Test:tests/sign_agent_card_deprecated.rs.
(sign_agent_card_ed25519, its Ed25519 twin, was added and removed within
0.15.0 and never published;sign_card_ed25519is the Ed25519 signer.) -
HotReloadAgentCardHandler::update, fortry_update(server;
spec §5.7: arrays marked as required MUST contain at least one element).
updatestored any card;try_updatechecks it asreload_from_json
checks a reloaded one (AgentCard::validateand
validate_security_schemes) and refuses withInvalidParams, leaving
the current card in place. -
ClientConfig::preferred_bindingsandClientConfig::with_preferred_bindings
(client). Nothing reads them onceClientBuilder::from_cardfollows the
card's interface order (spec §8.3.2); setting them changed nothing a caller
could see. Rank bindings withClientBuilder::from_card_preferring, and see
the choice withClientBuilder::chosen_interface. Kept for one minor
release, asSTABILITY.mdrequires. -
OAuthFlows::ImplicitandOAuthFlows::Password(types), as the A2A
1.0 proto marks both fieldsdeprecated. They remain, parse and serialize
as before (spec §1.4 keeps deprecated definitions until the next major
version); code that names them now gets a deprecation warning.
Security
-
TaskPushNotificationConfig'sDebugprints the webhook's origin, not
its URL (types; spec §13.2, §13.4). A webhook URL routinely carries the
receiver's secret in its path, query or userinfo;{:?}printed it whole,
into logs and panic messages, while the server's and client's own log
lines already printed onlyscheme://host[:port]. Tests:
push::tests::debug_redacts_secrets_but_keeps_presence(which asserted
the full URL and now asserts its absence),
push::tests::debug_url_never_echoes_what_it_cannot_cut. -
rsais no longer in any lockfile (RUSTSEC-2023-0071, Marvin timing
side channel, no fixed release). It was inCargo.lockonly through
sqlx-mysql0.8, a driver no feature enables, andosv-scanner.toml
waived it;sqlx0.9 does not bring it, so the waiver is removed. CI's
Go toolchain moves from 1.26.9 to 1.26.10 for GO-2026-6809
(crypto/x509), which govulncheck reported reachable in all three Go
interop modules underitk/; with 1.26.10 it reports none. -
tokiorequires 1.54.1; lockfiles move off this week's advisories
(client, server, SDK, SLIMRPC binding).tokio1.54.0, yanked for a use-after-free race intime::Intervalon
the multi-threaded runtime (GHSA-5wjr-9356-4wc8; the server's halt sync
usesInterval), is replaced by 1.54.1 in the workspace, SLIMRPC, fuzz
and ITK lockfiles.rustls-webpki0.103.15 (RUSTSEC-2026-0382, distrusted
emSign intermediates) is replaced by 0.103.16 wherever it was locked, and
the SDK-comparison harness'swebpki-root-certs1.0.9 (RUSTSEC-2026-0383)
by 1.0.10. The SLIMRPC lockfile'sctutils0.4.3 (RUSTSEC-2026-0385,
mask generation andu128comparisons that could branch on secret values)
becomes 0.4.4; it arrives through the SLIM crates'p256andml-kem,
and no other lockfile holds it. Itscrypto-bigint0.7.5
(RUSTSEC-2026-0387, operations meant to be constant-time that could
branch on secret values) becomes 0.7.6; it arrives through
agntcy-slim-auth'sp256, and no other lockfile holds it either. In
CI's hash-locked Python tools,certifi2026.7.22 (GHSA-6q48-6c7w-9gw2)
becomes 2026.10.19 anduv0.13.1 (GHSA-hq85-5cpj-mr62) becomes 0.13.5,
for the same emSign root. Thetokiorequirement rises from 1.44.2 to
1.54.1, so a downstream lockfile that already holds 1.54.0 must update
(crates.io has yanked it, so a new resolution never picked it; a
lockfile written between 2026-10-19 and the yank could). 1.54.1 needs
Rust 1.85, under this crate's 1.88. -
webpki-rootsrequires 1.0.10 (client, server; RUSTSEC-2026-0381,
affecting 1.0.0 to 1.0.9). The lockfiles already resolved 1.0.10, except
the SDK-comparison bench harness, which is updated; the manifests admitted
the affected versions to a downstream build that resolves its own.
check_advisory_floors.pyfound it. -
A keyed audit digest has one spelling (server,
auditand
encryption).AuditLog::digest_matchesdecoded the hex of a stored
hmac-sha256:<kid>:<hex>withu8::from_str_radix, which also takes
uppercase digits and a leading+, so a record whose digest had a digit
uppercased (or0fwritten+f) still matched. Nothing could be forged
this way, since the tag bytes compared were the same, but a changed record
matched. It now takes lowercase digits only, which is all the SDK writes.
Found by the newsealed_audit_valuefuzz target. Test:
hex_round_trips_and_refuses_what_is_not_hex. -
A control character in a client's id is refused (server; spec §14
"MUST sanitize user-provided content").validate_idrefused only empty
and over-long ids, so"a\r\nb"as a message, context, task or reference
task id was stored, andGetTask,CancelTaskandSubscribeToTask
logged the requested id with%, writing a line of the client's choosing
into the server's log. The push-configtaskIdandid(create, get,
list, delete, and inline inSendMessage) and theListTaskscontextId
filter never reachedvalidate_idat all. Each now answers
InvalidParamsnaming the field —-32602on JSON-RPC,400on
HTTP+JSON — for anychar::is_controlcharacter (C0, DEL, C1), as the
idempotency key already did; a tenant is refused likewise, not only for
U+001F. An inline push config is checked before the task is created. The
three entry logs and JSON-RPC's dispatch log now record the id and method
with?, escaped, since they run before validation. Letters outside ASCII
still pass. Tests:control_chars_in_ids,handler::ids::tests. -
JSON-RPC authenticates a call before reading its params (server;
docs/handoff.md"Open engineering"). A call whoseparamsdid not
deserialize was answered-32602, naming the field at fault, before the
interceptor chain saw it, so a caller with no credential got the parameter
schema where a well-formed call got401. Such a call is now admitted
first: one the chain refuses gets the refusal (401/403with its
challenge,-32600in the body;200inside a batch), and one it admits
gets the-32602it always got. Interceptors now see these calls, with no
task id, andon_completereports themFailedwith the-32602; a rate
limiter counts them and an audit interceptor records them. HTTP+JSON, gRPC
and WebSocket followed the same day (Behaviour Changes, "A malformed
request without a credential is refused as unauthenticated"). Test:
jsonrpc_auth_before_params. -
A card-signing key whose
x5ccertificate has expired is not used
(types,signing; spec §8.4.3 "Expired or revoked keys MUST NOT be used
for verification"; RFC 7517 §4.7).Jwkhad nox5cmember, so serde
dropped it and a key whose certificate had expired still verified a card.
Jwkgainsx5c: Option<Vec<String>>, andJwks::candidates(so
verify_agent_card_with_jwks,verify_card_with_jwks,
verify_card_json_with_jwksand the client's signed-card fetch) uses a
key that carries one only when its first certificate holds the same key
(algorithm, curve and key bits from the parsedSubjectPublicKeyInfo)
and the verification time is within itsnotBefore..notAfter,
inclusive. An empty chain, an entry that is not standard base64 or not
exactly one DER certificate leaves the key unused; the refusal reads as
a missing key. Keys withoutx5care unchanged. Not checked: the chain
to a trust anchor, the certificates' signatures, key usage, and
revocation (CRL/OCSP); the set itself is the caller's trusted source.
New dependency of thesigningfeature:x509-parser0.18
(MIT OR Apache-2.0, already in the lockfile throughrcgen), default
features off. Tests:signing::keys::x5c::tests,
signing::keys::x5c::spki_tests, and
a_fetched_key_whose_certificate_has_expired_is_not_usedin the
client'sjwks_fetch_tests. -
SLIMRPC: the binding has no advisory waivers (
a2a-protocol-slimrpc).
Withagntcy-slim-auth0.17.5 the lock takesmls-rs-crypto-awslc
0.25.1, which pinsaws-lc-rs =1.18.1, sorustlsresolves to 0.23.46
(from 0.23.43) and RUSTSEC-2026-0285, -0346, -0348 and -0349 no longer
apply;agntcy-slim-config0.17.5 andregorus0.12.0 replace
serde_yaml, sounsafe-libyaml(RUSTSEC-2024-0449) is gone. The five
waivers are removed from the binding'sdeny.tomland the six entries
from itsosv-scanner.toml; cargo-deny 0.20.2 and osv-scanner 2.6.0 report
nothing without them.deny.tomlallowsCDLA-Permissive-2.0again, as
the root does, forwebpki-root-certs, which the SLIM 3.x tree brings.
Since 2026-10-19 it carries one again:mls-rs-crypto-awslc0.25.1 was
yanked with no stated defect, and 0.25.0 would bring the four rustls
advisories back; the binding'sdeny.tomlexplains the entry. Update the
binding's lockfile by package name, never with a blanketcargo update,
which cannot keep a yanked version and downgrades rustls to 0.23.43.
Since 2026-10-23 it has none again:agntcy-slim-mls0.5.1 accepts
mls-rs-crypto-awslc0.26.0, so a plaincargo updatetakes
agntcy-slim-rpc3.4.6 and the unyanked 0.26.0 withrustlsstaying at
0.23.46, and cargo-deny reports the entry as no longer encountered. -
rustls0.23.46 is the floor (client, server, sdk). RustSec published
four rustls advisories on 2026-10-14, all patched in 0.23.46
(GHSA-w44r-93j7-qv28, an unauthenticated ServerHello downgrading later
connections to classical key exchange; GHSA-c8m5-c952-6cm4,Debugof
client session values printing TLS secrets; GHSA-vp88-798v-h67f, buffered
data sent to the outer server after an ECH rejection; GHSA-h2v8-394m-p7vv,
Brotli certificate decompression, only with rustls'sbrotlifeature).
The published manifests required>=0.23.45, so a consumer could keep an
affected rustls; they now require>=0.23.46, and the root and both
benchmark-harness lockfiles take 0.23.46.check_advisory_floors.pyfound
it: 8 requirement/advisory pairs, 0 after the change. The SLIMRPC binding
stays on 0.23.43 for RUSTSEC-2026-0285's reason (aws-lc-rs), and waives
the three that reach 0.23.43 in itsosv-scanner.toml; cargo-deny matches
waivers only by RUSTSEC id, which these advisories do not have yet. -
The client's debug output names headers and JSON keys, never their
values (client).ClientRequest,ClientResponse,CardFetchOptionsand
WebSocketTransportConfigderivedDebug, so{req:?}in an interceptor
printed theAuthorizationan auth interceptor had added, a
CardFetchOptionsprinted the bearer token its own example sets, and a
push-config call's request or response printed the webhook'stokenand
credentials. They print header names, sorted, and the top-level keys of
paramsandresultnow, as the server'sCallContextdoes; read a value
from its field when you want one. The fields' names in that output changed
with it:extra_header_names,header_names,param_keys,result_keys. -
A failed push delivery is logged with the webhook's origin, not its
URL (server). The background delivery's failure and timeout warnings
logged the whole webhook URL, so a secret in its path or query
(/hooks/<token>,?sig=) went into every log pipeline the server
feeds.HttpPushSender's own log lines already named the origin only. -
SLIMRPC: a caller's
:-prefixed metadata key is dropped
(a2a-protocol-slimrpc). A key beginning with:names what a server
records itself,":peer-addr"(the connection's address, for
CallContext::peer_addr) and":path", and the SLIMRPC server records
neither, but it passed a caller's own through as request headers. A caller
could therefore set the addressAuthFailureThrottlecharges its failed
attempts to, and lock out the HTTP client at that address when one handler
serves both. Its":path"gave no more than the path an HTTP caller
already chooses. Such keys are dropped now, beside SLIMRPC's own.
":peer-addr"is new in this release;":path"was passed through by
the binding's 0.6.0. -
A card signature naming a critical JWS extension is refused (types;
spec §8.4: card signatures are JWS "as defined in RFC 7515", whose
§4.1.11 makes a JWS invalid when itscritnames an extension the
recipient does not understand).signature_header, which
verify_agent_cardand the JWK Set verifiers read, ignoredcrit, so a
signature whose protected header carried"crit": ["exp"], "exp": …
verified, its expiry unenforced. This verifier understands no extension,
so anycrit, in the protected header or the unprotected one (where RFC
7515 forbids it), now fails verification. -
SLIMRPC binding: RUSTSEC-2024-0449 waived, with its chain recorded
(bindings/a2a-protocol-slimrpc/deny.toml).unsafe-libyamlis
unmaintained (an informational advisory, not a vulnerability) and reaches
only the binding, asserde_yamlunderagntcy-slim-configandregorus.
Everyagntcy-slim-configrelease through 0.17.2 still requires
serde_yaml, so no bump available here removes it; the entry names the
release that would. The four published crates do not contain it. -
Code-scanning findings on
mainaddressed (OSV-Scanner, Scorecard,
CodeQL; reproduced locally with the same tools before and after).- Test-peer dependencies: the Python peers floor
anyio>=4.14.2
(CVE-2026-63374, CVE-2026-64847) andh11>=0.16.0(CVE-2025-43859);
the JavaScript echo agent movesuuidto^11.1.1(GHSA-w5hq-g745-h8pq)
and gains a lockfile. The Java peer manages the Netty and Jackson
artifacts the advisories name directly as well as through BOMs (Maven
already resolved the fixed versions; OSV's source mode did not). A
recursiveosv-scannerover the tree finds no advisories. - Every Docker base image is pinned by digest; the Python and JavaScript
agent images install from hash-locked files (requirements.lock,
npm ci); CI'spip installsteps install from hash-locked files under
.github/requirements/. Scorecard Pinned-Dependencies: 23 findings to 0. osv.yml'srustjob runs the scanner inline so Scorecard sees its SARIF
upload: Token-Permissions 7 findings to 4 (the four remaining
contents: writegrants create releases and push the benchmark
dashboard and the nightly prerelease, which need them).- CodeQL
rust/cleartext-logging: fourjwt.rstest assertions no longer
format secret-derived values into their failure messages. - CodeQL Python code quality: 45 findings in the gate and probe scripts
fixed (unclosed files, silentexceptblocks, unused imports).
- Test-peer dependencies: the Python peers floor
-
Stored secrets are wiped on drop (
zeroize, already in the
dependency graph through rustls, so no new crate). This covers the HS256
secret held byJwtValidator, including one refused as too short, and the
client secret held byOAuth2ClientCredentials. The copies a request
makes while it is in flight are not wiped:ring's HMAC key, the token
request body, and hyper's buffers. The documentation on each field says
so. -
A server whose stores cannot isolate tenants now refuses requests that
name one. The defaultRequestHandlerBuilderstore ignored thetenant
field, so a request carrying one was served from records every tenant
shares: on 0.14.1, tenant B could get, list, subscribe to and cancel
tenant A's task (five of five probes,
docs/sdk-comparison-2026-10-02.md§5.1).TaskStoreand
PushConfigStoregainisolates_tenants(),falseby default andtrue
on the sixTenantAware*stores. A request that resolves to a non-empty
tenant — named by the client or derived by a resolver — is refused with
UnsupportedOperationunless both stores answertrue. Requests without
a tenant are unaffected. -
JwtValidatorrefuses an HS256 secret shorter than 32 bytes (server,
auth-jwtfeature). RFC 7518 §3.2 requires an HS256 key at least as long
as the hash output;with_hs256_secretaccepted any length, down to one
byte, so a guessable secret let anyone who guessed it mint tokens the
server accepted.with_hs256_secretnow fails closed for a short secret:
it logs an error and stores nothing, so every HS256 token is rejected. The
newtry_with_hs256_secretreturns an error instead, for refusing the
secret at startup, andMIN_HS256_SECRET_LENnames the bound. A
deployment configured with a short secret stops accepting HS256 tokens on
upgrade and must move to a secret of at least 32 bytes. RS256 keys under
2048 bits were already refused byring. The book's example, which taught
a 13-byte secret, andexamples/genai-agentnow use
try_with_hs256_secret.