Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
b348735
[DRAFT] feat: Upgrade A2A to v1.0
Tehsmash Nov 20, 2025
74c5a19
feat!: migrate from Pydantic types to protobuf-generated types
muscariello Nov 28, 2025
2d698df
fix: update E2E tests and push notification handlers for proto migration
muscariello Nov 29, 2025
424dd7e
fix: resolve all linter errors and add pyright type fixes
muscariello Dec 1, 2025
7405dc7
refactor: Remove redundant JSON-RPC Pydantic types, use jsonrpc libra…
muscariello Dec 1, 2025
6462801
Address PR review feedback: rename methods, update types, clean up al…
muscariello Dec 1, 2025
42c72f2
refactor: remove extras.py and consolidate error types in utils/error…
muscariello Dec 1, 2025
ac1050d
chore: remove AIP-discussion-response.md from tracking
muscariello Dec 1, 2025
7ea7475
fix: Improve streaming errors handling (#576)
lkawka Dec 3, 2025
174d58d
chore(main): release 0.3.20 (#577)
a2a-bot Dec 3, 2025
5fea21f
docs: Fixing typos (#586)
didier-durand Dec 12, 2025
8a76730
feat: Implement Agent Card Signing and Verification per Spec (#581)
sokoliva Dec 12, 2025
090ca9c
chore: Fixing typos (final round) (#588)
didier-durand Dec 12, 2025
03fa4c2
chore(main): release 0.3.21 (#587)
a2a-bot Dec 12, 2025
04bcafc
feat: Add custom ID generators to SimpleRequestContextBuilder (#594)
chenweiyang0204 Dec 16, 2025
e12ca42
test: adding 2 additional tests to user.py (#595)
didier-durand Dec 16, 2025
3deecc4
test: adding 21 tests for client/card_resolver.py (#592)
didier-durand Dec 16, 2025
6fa6a6c
refactor: Move agent card signature verification into `A2ACardResolve…
sokoliva Dec 16, 2025
86c6759
chore(main): release 0.3.22 (#599)
a2a-bot Dec 16, 2025
df78a94
test: adding 13 tests for id_generator.py (#591)
didier-durand Jan 5, 2026
cb7cdb3
chore(deps): bump the github-actions group across 1 directory with 4 …
dependabot[bot] Jan 5, 2026
4487307
fix: align tests and implementation with proto definition updates
muscariello Jan 16, 2026
a680c18
chore: merge main and fix tests for proto refactor
muscariello Jan 16, 2026
0ae8548
style: fix spelling errors and add words to allow list
muscariello Jan 17, 2026
bd552f6
style: add more tech terms to spelling allow list
muscariello Jan 17, 2026
d8df048
style: fix spelling of interruptible
muscariello Jan 17, 2026
7433e04
build(spelling): exclude generated protobuf types from spell check
muscariello Jan 17, 2026
8e5ab33
fix: replace non-inclusive language in optionals.py
muscariello Jan 17, 2026
ce4c828
chore: remove unused type generation scripts and deps
muscariello Jan 19, 2026
66073cf
Apply suggestions from code review
muscariello Jan 20, 2026
d7fb690
Apply suggestion from @Tehsmash
muscariello Jan 20, 2026
99bb2c8
refactor: address PR review comments
muscariello Jan 20, 2026
3eeea28
fix(client): respect blocking configuration in send_message
muscariello Jan 20, 2026
796d86b
refactor: decouple JSON-RPC errors and fix circular imports
muscariello Jan 20, 2026
913234c
Apply suggestion from @Tehsmash
muscariello Jan 20, 2026
6260ea2
fix(server): add missing StreamResponse import in default_request_han…
muscariello Jan 20, 2026
c00b7b8
fix(server): wrap task in StreamResponse for push notifications
muscariello Jan 20, 2026
30685cf
Refactor: Extract JSONRPC error models to resolve circular dependency
muscariello Jan 20, 2026
33d3232
Docs: Clarify Proto3 repeated field presence checks in AuthInterceptor
muscariello Jan 20, 2026
f049d33
build: generate a2a.json OpenAPI spec via hatch build script
muscariello Jan 20, 2026
51ec5e9
chore: fix inclusive language and spelling check failures
muscariello Jan 20, 2026
6ea324b
chore: update spelling allow list and excludes
muscariello Jan 20, 2026
688e2c2
chore: add a2a-specific terms to spelling allow list
muscariello Jan 20, 2026
e9152ab
chore: add remaining unrecognized words to allow list
muscariello Jan 20, 2026
c6d97fd
chore: replace master with main in documentation URLs
muscariello Jan 20, 2026
cd8946b
chore: sort spelling allow list
muscariello Jan 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions buf.gen.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@ plugins:
# Generate python protobuf related code
# Generates *_pb2.py files, one for each .proto
- remote: buf.build/protocolbuffers/python:v29.3
out: src/a2a/grpc
out: src/a2a/types
# Generate python service code.
# Generates *_pb2_grpc.py
- remote: buf.build/grpc/python
out: src/a2a/grpc
out: src/a2a/types
# Generates *_pb2.pyi files.
- remote: buf.build/protocolbuffers/pyi
out: src/a2a/grpc
out: src/a2a/types
28 changes: 23 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ dependencies = [
"pydantic>=2.11.3",
"protobuf>=5.29.5",
"google-api-core>=1.26.0",
"json-rpc>=1.15.0",
"googleapis-common-protos>=1.70.0",
]

classifiers = [
Expand Down Expand Up @@ -74,6 +76,16 @@ addopts = "-ra --strict-markers"
markers = [
"asyncio: mark a test as a coroutine that should be run by pytest-asyncio",
]
filterwarnings = [
# SQLAlchemy warning about duplicate class registration - this is a known limitation
# of the dynamic model creation pattern used in models.py for custom table names
"ignore:This declarative base already contains a class with the same class name:sqlalchemy.exc.SAWarning",
# ResourceWarnings from asyncio event loop/socket cleanup during garbage collection
# These appear intermittently between tests due to pytest-asyncio and sse-starlette timing
"ignore:unclosed event loop:ResourceWarning",
"ignore:unclosed transport:ResourceWarning",
"ignore:unclosed <socket.socket:ResourceWarning",
]

[tool.pytest-asyncio]
mode = "strict"
Expand Down Expand Up @@ -114,7 +126,7 @@ explicit = true

[tool.mypy]
plugins = ["pydantic.mypy"]
exclude = ["src/a2a/grpc/"]
exclude = ["src/a2a/types/a2a_pb2\\.py", "src/a2a/types/a2a_pb2_grpc\\.py"]
disable_error_code = [
"import-not-found",
"annotation-unchecked",
Expand All @@ -134,7 +146,8 @@ exclude = [
"**/node_modules",
"**/venv",
"**/.venv",
"src/a2a/grpc/",
"src/a2a/types/a2a_pb2.py",
"src/a2a/types/a2a_pb2_grpc.py",
]
reportMissingImports = "none"
reportMissingModuleSource = "none"
Expand All @@ -145,7 +158,8 @@ omit = [
"*/tests/*",
"*/site-packages/*",
"*/__init__.py",
"src/a2a/grpc/*",
"src/a2a/types/a2a_pb2.py",
"src/a2a/types/a2a_pb2_grpc.py",
]

[tool.coverage.report]
Expand Down Expand Up @@ -257,7 +271,9 @@ exclude = [
"node_modules",
"venv",
"*/migrations/*",
"src/a2a/grpc/**",
"src/a2a/types/a2a_pb2.py",
"src/a2a/types/a2a_pb2.pyi",
"src/a2a/types/a2a_pb2_grpc.py",
"tests/**",
]

Expand Down Expand Up @@ -311,7 +327,9 @@ inline-quotes = "single"

[tool.ruff.format]
exclude = [
"src/a2a/grpc/**",
"src/a2a/types/a2a_pb2.py",
"src/a2a/types/a2a_pb2.pyi",
"src/a2a/types/a2a_pb2_grpc.py",
]
docstring-code-format = true
docstring-code-line-length = "dynamic"
Expand Down
21 changes: 0 additions & 21 deletions src/a2a/client/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,39 +18,18 @@
A2AClientTimeoutError,
)
from a2a.client.helpers import create_text_message_object
from a2a.client.legacy import A2AClient
from a2a.client.middleware import ClientCallContext, ClientCallInterceptor


logger = logging.getLogger(__name__)

try:
from a2a.client.legacy_grpc import A2AGrpcClient # type: ignore
except ImportError as e:
_original_error = e
logger.debug(
'A2AGrpcClient not loaded. This is expected if gRPC dependencies are not installed. Error: %s',
_original_error,
)

class A2AGrpcClient: # type: ignore
"""Placeholder for A2AGrpcClient when dependencies are not installed."""

def __init__(self, *args, **kwargs):
raise ImportError(
'To use A2AGrpcClient, its dependencies must be installed. '
'You can install them with \'pip install "a2a-sdk[grpc]"\''
) from _original_error


__all__ = [
'A2ACardResolver',
'A2AClient',
'A2AClientError',
'A2AClientHTTPError',
'A2AClientJSONError',
'A2AClientTimeoutError',
'A2AGrpcClient',
'AuthInterceptor',
'BaseClient',
'Client',
Expand Down
96 changes: 45 additions & 51 deletions src/a2a/client/auth/interceptor.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,7 @@

from a2a.client.auth.credentials import CredentialService
from a2a.client.middleware import ClientCallContext, ClientCallInterceptor
from a2a.types import (
AgentCard,
APIKeySecurityScheme,
HTTPAuthSecurityScheme,
In,
OAuth2SecurityScheme,
OpenIdConnectSecurityScheme,
)
from a2a.types.a2a_pb2 import AgentCard

logger = logging.getLogger(__name__)

Expand All @@ -35,63 +28,64 @@ async def intercept(
"""Applies authentication headers to the request if credentials are available."""
if (
agent_card is None
or agent_card.security is None
or agent_card.security_schemes is None
or not agent_card.security
or not agent_card.security_schemes
):
return request_payload, http_kwargs

for requirement in agent_card.security:
for scheme_name in requirement:
for scheme_name in requirement.schemes:
credential = await self._credential_service.get_credentials(
scheme_name, context
)
if credential and scheme_name in agent_card.security_schemes:
scheme_def_union = agent_card.security_schemes.get(
scheme_name
)
if not scheme_def_union:
scheme = agent_card.security_schemes.get(scheme_name)
if not scheme:
continue
scheme_def = scheme_def_union.root

headers = http_kwargs.get('headers', {})

match scheme_def:
# Case 1a: HTTP Bearer scheme with an if guard
case HTTPAuthSecurityScheme() if (
scheme_def.scheme.lower() == 'bearer'
):
headers['Authorization'] = f'Bearer {credential}'
logger.debug(
"Added Bearer token for scheme '%s' (type: %s).",
scheme_name,
scheme_def.type,
)
http_kwargs['headers'] = headers
return request_payload, http_kwargs
# HTTP Bearer authentication
if (
scheme.HasField('http_auth_security_scheme')
and scheme.http_auth_security_scheme.scheme.lower()
== 'bearer'
):
headers['Authorization'] = f'Bearer {credential}'
logger.debug(
"Added Bearer token for scheme '%s'.",
scheme_name,
)
http_kwargs['headers'] = headers
return request_payload, http_kwargs

# Case 1b: OAuth2 and OIDC schemes, which are implicitly Bearer
case (
OAuth2SecurityScheme()
| OpenIdConnectSecurityScheme()
):
headers['Authorization'] = f'Bearer {credential}'
logger.debug(
"Added Bearer token for scheme '%s' (type: %s).",
scheme_name,
scheme_def.type,
)
http_kwargs['headers'] = headers
return request_payload, http_kwargs
# OAuth2 and OIDC schemes are implicitly Bearer
if scheme.HasField(
'oauth2_security_scheme'
) or scheme.HasField('open_id_connect_security_scheme'):
headers['Authorization'] = f'Bearer {credential}'
logger.debug(
"Added Bearer token for scheme '%s'.",
scheme_name,
)
http_kwargs['headers'] = headers
return request_payload, http_kwargs

# Case 2: API Key in Header
case APIKeySecurityScheme(in_=In.header):
headers[scheme_def.name] = credential
logger.debug(
"Added API Key Header for scheme '%s'.",
scheme_name,
)
http_kwargs['headers'] = headers
return request_payload, http_kwargs
# API Key in Header
if (
scheme.HasField('api_key_security_scheme')
and scheme.api_key_security_scheme.location.lower()
== 'header'
):
headers[scheme.api_key_security_scheme.name] = (
credential
)
logger.debug(
"Added API Key Header for scheme '%s'.",
scheme_name,
)
http_kwargs['headers'] = headers
return request_payload, http_kwargs

# Note: Other cases like API keys in query/cookie are not handled and will be skipped.

Expand Down
Loading