Skip to content

Commit 210476b

Browse files
authored
Merge branch 'main' into restful
2 parents c2f4f2f + cd94167 commit 210476b

File tree

9 files changed

+169
-158
lines changed

9 files changed

+169
-158
lines changed

.github/actions/spelling/allow.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ genai
3939
getkwargs
4040
gle
4141
GVsb
42+
ietf
4243
initdb
4344
inmemory
4445
INR

scripts/format.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ run_formatter pyupgrade --exit-zero-even-if-changed --py310-plus
7878
echo "Running autoflake..."
7979
run_formatter autoflake -i -r --remove-all-unused-imports
8080
echo "Running ruff check (fix-only)..."
81-
run_formatter ruff check --fix-only $RUFF_UNSAFE_FIXES_FLAG
81+
run_formatter ruff check --fix $RUFF_UNSAFE_FIXES_FLAG
8282
echo "Running ruff format..."
8383
run_formatter ruff format
8484

src/a2a/_base.py

Lines changed: 0 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,3 @@
1-
import warnings
2-
3-
from typing import Any, ClassVar
4-
51
from pydantic import BaseModel, ConfigDict
62
from pydantic.alias_generators import to_camel
73

@@ -40,64 +36,3 @@ class A2ABaseModel(BaseModel):
4036
serialize_by_alias=True,
4137
alias_generator=to_camel_custom,
4238
)
43-
44-
# Cache for the alias -> field_name mapping.
45-
# It starts as None and is populated on first access.
46-
_alias_to_field_name_map: ClassVar[dict[str, str] | None] = None
47-
48-
@classmethod
49-
def _get_alias_map(cls) -> dict[str, str]:
50-
"""Lazily builds and returns the alias-to-field-name mapping for the class.
51-
52-
The map is cached on the class object to avoid re-computation.
53-
"""
54-
if cls._alias_to_field_name_map is None:
55-
cls._alias_to_field_name_map = {
56-
field.alias: field_name
57-
for field_name, field in cls.model_fields.items()
58-
if field.alias is not None
59-
}
60-
return cls._alias_to_field_name_map
61-
62-
def __setattr__(self, name: str, value: Any) -> None:
63-
"""Allow setting attributes via their camelCase alias."""
64-
# Get the map and find the corresponding snake_case field name.
65-
field_name = type(self)._get_alias_map().get(name) # noqa: SLF001
66-
67-
if field_name and field_name != name:
68-
# An alias was used, issue a warning.
69-
warnings.warn(
70-
(
71-
f"Setting field '{name}' via its camelCase alias is deprecated and will be removed in version 0.3.0 "
72-
f"Use the snake_case name '{field_name}' instead."
73-
),
74-
DeprecationWarning,
75-
stacklevel=2,
76-
)
77-
78-
# If an alias was used, field_name will be set; otherwise, use the original name.
79-
super().__setattr__(field_name or name, value)
80-
81-
def __getattr__(self, name: str) -> Any:
82-
"""Allow getting attributes via their camelCase alias."""
83-
# Get the map and find the corresponding snake_case field name.
84-
field_name = type(self)._get_alias_map().get(name) # noqa: SLF001
85-
86-
if field_name and field_name != name:
87-
# An alias was used, issue a warning.
88-
warnings.warn(
89-
(
90-
f"Accessing field '{name}' via its camelCase alias is deprecated and will be removed in version 0.3.0 "
91-
f"Use the snake_case name '{field_name}' instead."
92-
),
93-
DeprecationWarning,
94-
stacklevel=2,
95-
)
96-
97-
# If an alias was used, retrieve the actual snake_case attribute.
98-
return getattr(self, field_name)
99-
100-
# If it's not a known alias, it's a genuine missing attribute.
101-
raise AttributeError(
102-
f"'{type(self).__name__}' object has no attribute '{name}'"
103-
)

src/a2a/grpc/a2a_pb2.py

Lines changed: 72 additions & 70 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/a2a/grpc/a2a_pb2.pyi

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -277,22 +277,24 @@ class AgentExtension(_message.Message):
277277
def __init__(self, uri: _Optional[str] = ..., description: _Optional[str] = ..., required: bool = ..., params: _Optional[_Union[_struct_pb2.Struct, _Mapping]] = ...) -> None: ...
278278

279279
class AgentSkill(_message.Message):
280-
__slots__ = ("id", "name", "description", "tags", "examples", "input_modes", "output_modes")
280+
__slots__ = ("id", "name", "description", "tags", "examples", "input_modes", "output_modes", "security")
281281
ID_FIELD_NUMBER: _ClassVar[int]
282282
NAME_FIELD_NUMBER: _ClassVar[int]
283283
DESCRIPTION_FIELD_NUMBER: _ClassVar[int]
284284
TAGS_FIELD_NUMBER: _ClassVar[int]
285285
EXAMPLES_FIELD_NUMBER: _ClassVar[int]
286286
INPUT_MODES_FIELD_NUMBER: _ClassVar[int]
287287
OUTPUT_MODES_FIELD_NUMBER: _ClassVar[int]
288+
SECURITY_FIELD_NUMBER: _ClassVar[int]
288289
id: str
289290
name: str
290291
description: str
291292
tags: _containers.RepeatedScalarFieldContainer[str]
292293
examples: _containers.RepeatedScalarFieldContainer[str]
293294
input_modes: _containers.RepeatedScalarFieldContainer[str]
294295
output_modes: _containers.RepeatedScalarFieldContainer[str]
295-
def __init__(self, id: _Optional[str] = ..., name: _Optional[str] = ..., description: _Optional[str] = ..., tags: _Optional[_Iterable[str]] = ..., examples: _Optional[_Iterable[str]] = ..., input_modes: _Optional[_Iterable[str]] = ..., output_modes: _Optional[_Iterable[str]] = ...) -> None: ...
296+
security: _containers.RepeatedCompositeFieldContainer[Security]
297+
def __init__(self, id: _Optional[str] = ..., name: _Optional[str] = ..., description: _Optional[str] = ..., tags: _Optional[_Iterable[str]] = ..., examples: _Optional[_Iterable[str]] = ..., input_modes: _Optional[_Iterable[str]] = ..., output_modes: _Optional[_Iterable[str]] = ..., security: _Optional[_Iterable[_Union[Security, _Mapping]]] = ...) -> None: ...
296298

297299
class AgentCardSignature(_message.Message):
298300
__slots__ = ("protected", "signature", "header")
@@ -332,16 +334,18 @@ class Security(_message.Message):
332334
def __init__(self, schemes: _Optional[_Mapping[str, StringList]] = ...) -> None: ...
333335

334336
class SecurityScheme(_message.Message):
335-
__slots__ = ("api_key_security_scheme", "http_auth_security_scheme", "oauth2_security_scheme", "open_id_connect_security_scheme")
337+
__slots__ = ("api_key_security_scheme", "http_auth_security_scheme", "oauth2_security_scheme", "open_id_connect_security_scheme", "mtls_security_scheme")
336338
API_KEY_SECURITY_SCHEME_FIELD_NUMBER: _ClassVar[int]
337339
HTTP_AUTH_SECURITY_SCHEME_FIELD_NUMBER: _ClassVar[int]
338340
OAUTH2_SECURITY_SCHEME_FIELD_NUMBER: _ClassVar[int]
339341
OPEN_ID_CONNECT_SECURITY_SCHEME_FIELD_NUMBER: _ClassVar[int]
342+
MTLS_SECURITY_SCHEME_FIELD_NUMBER: _ClassVar[int]
340343
api_key_security_scheme: APIKeySecurityScheme
341344
http_auth_security_scheme: HTTPAuthSecurityScheme
342345
oauth2_security_scheme: OAuth2SecurityScheme
343346
open_id_connect_security_scheme: OpenIdConnectSecurityScheme
344-
def __init__(self, api_key_security_scheme: _Optional[_Union[APIKeySecurityScheme, _Mapping]] = ..., http_auth_security_scheme: _Optional[_Union[HTTPAuthSecurityScheme, _Mapping]] = ..., oauth2_security_scheme: _Optional[_Union[OAuth2SecurityScheme, _Mapping]] = ..., open_id_connect_security_scheme: _Optional[_Union[OpenIdConnectSecurityScheme, _Mapping]] = ...) -> None: ...
347+
mtls_security_scheme: MutualTlsSecurityScheme
348+
def __init__(self, api_key_security_scheme: _Optional[_Union[APIKeySecurityScheme, _Mapping]] = ..., http_auth_security_scheme: _Optional[_Union[HTTPAuthSecurityScheme, _Mapping]] = ..., oauth2_security_scheme: _Optional[_Union[OAuth2SecurityScheme, _Mapping]] = ..., open_id_connect_security_scheme: _Optional[_Union[OpenIdConnectSecurityScheme, _Mapping]] = ..., mtls_security_scheme: _Optional[_Union[MutualTlsSecurityScheme, _Mapping]] = ...) -> None: ...
345349

346350
class APIKeySecurityScheme(_message.Message):
347351
__slots__ = ("description", "location", "name")
@@ -364,12 +368,14 @@ class HTTPAuthSecurityScheme(_message.Message):
364368
def __init__(self, description: _Optional[str] = ..., scheme: _Optional[str] = ..., bearer_format: _Optional[str] = ...) -> None: ...
365369

366370
class OAuth2SecurityScheme(_message.Message):
367-
__slots__ = ("description", "flows")
371+
__slots__ = ("description", "flows", "oauth2_metadata_url")
368372
DESCRIPTION_FIELD_NUMBER: _ClassVar[int]
369373
FLOWS_FIELD_NUMBER: _ClassVar[int]
374+
OAUTH2_METADATA_URL_FIELD_NUMBER: _ClassVar[int]
370375
description: str
371376
flows: OAuthFlows
372-
def __init__(self, description: _Optional[str] = ..., flows: _Optional[_Union[OAuthFlows, _Mapping]] = ...) -> None: ...
377+
oauth2_metadata_url: str
378+
def __init__(self, description: _Optional[str] = ..., flows: _Optional[_Union[OAuthFlows, _Mapping]] = ..., oauth2_metadata_url: _Optional[str] = ...) -> None: ...
373379

374380
class OpenIdConnectSecurityScheme(_message.Message):
375381
__slots__ = ("description", "open_id_connect_url")
@@ -379,6 +385,12 @@ class OpenIdConnectSecurityScheme(_message.Message):
379385
open_id_connect_url: str
380386
def __init__(self, description: _Optional[str] = ..., open_id_connect_url: _Optional[str] = ...) -> None: ...
381387

388+
class MutualTlsSecurityScheme(_message.Message):
389+
__slots__ = ("description",)
390+
DESCRIPTION_FIELD_NUMBER: _ClassVar[int]
391+
description: str
392+
def __init__(self, description: _Optional[str] = ...) -> None: ...
393+
382394
class OAuthFlows(_message.Message):
383395
__slots__ = ("authorization_code", "client_credentials", "implicit", "password")
384396
AUTHORIZATION_CODE_FIELD_NUMBER: _ClassVar[int]

src/a2a/server/tasks/database_push_notification_config_store.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
# ruff: noqa: PLC0415
12
import json
23
import logging
34

@@ -94,7 +95,7 @@ def __init__(
9495

9596
if encryption_key:
9697
try:
97-
from cryptography.fernet import Fernet # noqa: PLC0415
98+
from cryptography.fernet import Fernet
9899
except ImportError as e:
99100
raise ImportError(
100101
"DatabasePushNotificationConfigStore with encryption requires the 'cryptography' "
@@ -166,7 +167,7 @@ def _from_orm(
166167
payload = model_instance.config_data
167168

168169
if self._fernet:
169-
from cryptography.fernet import InvalidToken # noqa: PLC0415
170+
from cryptography.fernet import InvalidToken
170171

171172
try:
172173
decrypted_payload = self._fernet.decrypt(payload)

src/a2a/types.py

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,15 @@ class AgentSkill(A2ABaseModel):
164164
"""
165165
The set of supported output MIME types for this skill, overriding the agent's defaults.
166166
"""
167+
security: list[dict[str, list[str]]] | None = Field(
168+
default=None, examples=[[{'google': ['oidc']}]]
169+
)
170+
"""
171+
Security schemes necessary for the agent to leverage this skill.
172+
As in the overall AgentCard.security, this list represents a logical OR of security
173+
requirement objects. Each object is a set of security schemes that must be used together
174+
(a logical AND).
175+
"""
167176
tags: list[str] = Field(
168177
..., examples=[['cooking', 'customer support', 'billing']]
169178
)
@@ -730,6 +739,21 @@ class MethodNotFoundError(A2ABaseModel):
730739
"""
731740

732741

742+
class MutualTLSSecurityScheme(A2ABaseModel):
743+
"""
744+
Defines a security scheme using mTLS authentication.
745+
"""
746+
747+
description: str | None = None
748+
"""
749+
An optional description for the security scheme.
750+
"""
751+
type: Literal['mutualTLS'] = 'mutualTLS'
752+
"""
753+
The type of the security scheme. Must be 'mutualTLS'.
754+
"""
755+
756+
733757
class OpenIdConnectSecurityScheme(A2ABaseModel):
734758
"""
735759
Defines a security scheme using OpenID Connect.
@@ -1486,6 +1510,11 @@ class OAuth2SecurityScheme(A2ABaseModel):
14861510
"""
14871511
An object containing configuration information for the supported OAuth 2.0 flows.
14881512
"""
1513+
oauth2_metadata_url: str | None = None
1514+
"""
1515+
URL to the oauth2 authorization server metadata
1516+
[RFC8414](https://datatracker.ietf.org/doc/html/rfc8414). TLS is required.
1517+
"""
14891518
type: Literal['oauth2'] = 'oauth2'
14901519
"""
14911520
The type of the security scheme. Must be 'oauth2'.
@@ -1498,13 +1527,15 @@ class SecurityScheme(
14981527
| HTTPAuthSecurityScheme
14991528
| OAuth2SecurityScheme
15001529
| OpenIdConnectSecurityScheme
1530+
| MutualTLSSecurityScheme
15011531
]
15021532
):
15031533
root: (
15041534
APIKeySecurityScheme
15051535
| HTTPAuthSecurityScheme
15061536
| OAuth2SecurityScheme
15071537
| OpenIdConnectSecurityScheme
1538+
| MutualTLSSecurityScheme
15081539
)
15091540
"""
15101541
Defines a security scheme that can be used to secure an agent's endpoints.
@@ -1754,18 +1785,24 @@ class AgentCard(A2ABaseModel):
17541785
This creates a binding between the main URL and its supported transport protocol.
17551786
Clients should prefer this transport and URL combination when both are supported.
17561787
"""
1757-
protocol_version: str | None = '0.2.6'
1788+
protocol_version: str | None = '0.3.0'
17581789
"""
17591790
The version of the A2A protocol this agent supports.
17601791
"""
17611792
provider: AgentProvider | None = None
17621793
"""
17631794
Information about the agent's service provider.
17641795
"""
1765-
security: list[dict[str, list[str]]] | None = None
1796+
security: list[dict[str, list[str]]] | None = Field(
1797+
default=None,
1798+
examples=[[{'oauth': ['read']}, {'api-key': [], 'mtls': []}]],
1799+
)
17661800
"""
17671801
A list of security requirement objects that apply to all agent interactions. Each object
17681802
lists security schemes that can be used. Follows the OpenAPI 3.0 Security Requirement Object.
1803+
This list can be seen as an OR of ANDs. Each object in the list describes one possible
1804+
set of security requirements that must be present on a request. This allows specifying,
1805+
for example, "callers must either use OAuth OR an API Key AND mTLS."
17691806
"""
17701807
security_schemes: dict[str, SecurityScheme] | None = None
17711808
"""

src/a2a/utils/proto_utils.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,12 @@ def security_scheme(
386386
flows=cls.oauth2_flows(scheme.root.flows),
387387
)
388388
)
389+
if isinstance(scheme.root, types.MutualTLSSecurityScheme):
390+
return a2a_pb2.SecurityScheme(
391+
mtls_security_scheme=a2a_pb2.MutualTlsSecurityScheme(
392+
description=scheme.root.description,
393+
)
394+
)
389395
return a2a_pb2.SecurityScheme(
390396
open_id_connect_security_scheme=a2a_pb2.OpenIdConnectSecurityScheme(
391397
description=scheme.root.description,

tests/test_types.py

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,11 @@
2222
FilePart,
2323
FileWithBytes,
2424
FileWithUri,
25-
GetTaskPushNotificationConfigParams,
26-
GetTaskPushNotificationConfigRequest,
2725
GetAuthenticatedExtendedCardRequest,
2826
GetAuthenticatedExtendedCardResponse,
2927
GetAuthenticatedExtendedCardSuccessResponse,
28+
GetTaskPushNotificationConfigParams,
29+
GetTaskPushNotificationConfigRequest,
3030
GetTaskPushNotificationConfigResponse,
3131
GetTaskPushNotificationConfigSuccessResponse,
3232
GetTaskRequest,
@@ -1556,7 +1556,11 @@ def test_use_get_task_push_notification_params_for_request() -> None:
15561556
)
15571557

15581558

1559-
def test_camelCase() -> None:
1559+
def test_camelCase_access_raises_attribute_error() -> None:
1560+
"""
1561+
Tests that accessing or setting fields via their camelCase alias
1562+
raises an AttributeError.
1563+
"""
15601564
skill = AgentSkill(
15611565
id='hello_world',
15621566
name='Returns hello world',
@@ -1565,6 +1569,7 @@ def test_camelCase() -> None:
15651569
examples=['hi', 'hello world'],
15661570
)
15671571

1572+
# Initialization with camelCase still works due to Pydantic's populate_by_name config
15681573
agent_card = AgentCard(
15691574
name='Hello World Agent',
15701575
description='Just a hello world agent',
@@ -1577,21 +1582,33 @@ def test_camelCase() -> None:
15771582
supportsAuthenticatedExtendedCard=True, # type: ignore
15781583
)
15791584

1580-
# Test setting an attribute via camelCase alias
1581-
with pytest.warns(
1582-
DeprecationWarning,
1583-
match="Setting field 'supportsAuthenticatedExtendedCard'",
1585+
# --- Test that using camelCase aliases raises errors ---
1586+
1587+
# Test setting an attribute via camelCase alias raises AttributeError
1588+
with pytest.raises(
1589+
ValueError,
1590+
match='"AgentCard" object has no field "supportsAuthenticatedExtendedCard"',
15841591
):
15851592
agent_card.supportsAuthenticatedExtendedCard = False
15861593

1587-
# Test getting an attribute via camelCase alias
1588-
with pytest.warns(
1589-
DeprecationWarning, match="Accessing field 'defaultInputModes'"
1594+
# Test getting an attribute via camelCase alias raises AttributeError
1595+
with pytest.raises(
1596+
AttributeError,
1597+
match="'AgentCard' object has no attribute 'defaultInputModes'",
15901598
):
1591-
default_input_modes = agent_card.defaultInputModes
1599+
_ = agent_card.defaultInputModes
1600+
1601+
# --- Test that using snake_case names works correctly ---
15921602

1593-
# Assert the functionality still works as expected
1603+
# The value should be unchanged because the camelCase setattr failed
1604+
assert agent_card.supports_authenticated_extended_card is True
1605+
1606+
# Now, set it correctly using the snake_case name
1607+
agent_card.supports_authenticated_extended_card = False
15941608
assert agent_card.supports_authenticated_extended_card is False
1609+
1610+
# Get the attribute correctly using the snake_case name
1611+
default_input_modes = agent_card.default_input_modes
15951612
assert default_input_modes == ['text']
15961613
assert agent_card.default_input_modes == ['text']
15971614

0 commit comments

Comments
 (0)