1
1
from typing import Optional , Dict , Any , List , Callable
2
2
from pydantic import BaseModel , Field , ValidationError , ConfigDict
3
3
from copy import deepcopy
4
- import logging
5
4
6
- # --- Import official A2A types ---
7
- # Assuming the official A2A message type is available here:
8
- try :
9
- from a2a .types import A2AMessage
10
- except ImportError :
11
- # Fallback/Placeholder if official package is not fully installed locally
12
- class A2AMessage (BaseModel ):
13
- metadata : Dict [str , Any ] = Field (default_factory = dict )
14
-
15
5
# --- Extension Definition ---
16
6
17
- SECURE_PASSPORT_URI = "https://a2a-protocol.org/ext/ secure-passport/v1 "
7
+ SECURE_PASSPORT_URI = "https://github.com/a2aproject/ a2a-samples/tree/main/samples/python/extensions/ secure-passport"
18
8
19
9
class CallerContext (BaseModel ):
20
10
"""
21
11
The Secure Passport payload containing contextual state shared by the calling agent.
22
12
"""
23
- # PEP 8 Compliant (snake_case) with JSON mapping (camelCase alias)
24
- agent_id : str = Field (..., alias = 'agentId ' , description = "The verifiable unique identifier of the calling agent ." )
13
+ # *** CORE CHANGE: agent_id renamed to client_id ***
14
+ client_id : str = Field (..., alias = 'clientId ' , description = "The verifiable unique identifier of the calling client ." )
25
15
signature : Optional [str ] = Field (None , alias = 'signature' , description = "A cryptographic signature of the 'state' payload." )
26
16
session_id : Optional [str ] = Field (None , alias = 'sessionId' , description = "A session or conversation identifier for continuity." )
27
17
state : Dict [str , Any ] = Field (..., description = "A free-form JSON object containing the contextual data." )
28
18
29
19
# Use ConfigDict for Pydantic V2 compatibility and configuration
30
20
model_config = ConfigDict (
31
- # Allows instantiation using either the JSON alias or the Python field name
32
21
populate_by_name = True ,
33
22
extra = 'forbid'
34
23
)
@@ -41,11 +30,18 @@ def is_verified(self) -> bool:
41
30
return self .signature is not None
42
31
43
32
# --- Helper Functions (Core Protocol Interaction) ---
33
+
34
+ class BaseA2AMessage (BaseModel ):
35
+ metadata : Dict [str , Any ] = Field (default_factory = dict )
36
+
37
+ try :
38
+ from a2a .types import A2AMessage
39
+ except ImportError :
40
+ A2AMessage = BaseA2AMessage
44
41
45
42
def add_secure_passport (message : A2AMessage , context : CallerContext ) -> None :
46
43
"""Adds the Secure Passport (CallerContext) to the message's metadata."""
47
44
48
- # by_alias=True ensures the output JSON uses the correct camelCase names
49
45
message .metadata [SECURE_PASSPORT_URI ] = context .model_dump (by_alias = True , exclude_none = True )
50
46
51
47
def get_secure_passport (message : A2AMessage ) -> Optional [CallerContext ]:
@@ -55,10 +51,9 @@ def get_secure_passport(message: A2AMessage) -> Optional[CallerContext]:
55
51
return None
56
52
57
53
try :
58
- # validate uses aliases implicitly for input conversion
59
54
return CallerContext .model_validate (deepcopy (passport_data ))
60
55
except ValidationError as e :
61
- # logging is already used here
56
+ import logging
62
57
logging .warning (f"ERROR: Received malformed Secure Passport data. Ignoring payload: { e } " )
63
58
return None
64
59
@@ -81,36 +76,33 @@ def get_agent_card_declaration(supported_state_keys: Optional[List[str]] = None)
81
76
"uri" : SECURE_PASSPORT_URI ,
82
77
"params" : {}
83
78
}
84
-
85
79
if supported_state_keys :
86
80
declaration ["params" ]["supportedStateKeys" ] = supported_state_keys
87
-
81
+
88
82
return declaration
89
83
90
84
@staticmethod
91
85
def client_middleware (next_handler : Callable [[A2AMessage ], Any ], message : A2AMessage , context : CallerContext ):
92
86
"""
93
87
[Conceptual Middleware Layer: Client/Calling Agent]
94
-
95
- Type Hint: next_handler takes one A2AMessage and returns Any.
96
88
"""
97
- logging .info (f"[Middleware: Client] Attaching Secure Passport for { context .agent_id } " )
89
+ # ACCESS UPDATED: Use context.client_id
90
+ print (f"[Middleware: Client] Attaching Secure Passport for { context .client_id } " )
98
91
add_secure_passport (message , context )
99
- return next_handler (message ) # Passes the augmented message to the transport layer
92
+ return next_handler (message )
100
93
101
94
@staticmethod
102
95
def server_middleware (next_handler : Callable [[A2AMessage , Optional [CallerContext ]], Any ], message : A2AMessage ):
103
96
"""
104
97
[Conceptual Middleware Layer: Server/Receiving Agent]
105
-
106
- Type Hint: next_handler takes A2AMessage and Optional[CallerContext] and returns Any.
107
98
"""
108
99
passport = get_secure_passport (message )
109
100
110
101
if passport :
111
- logging . info (f"[Middleware: Server] Extracted Secure Passport. Verified: { passport .is_verified } " )
102
+ print (f"[Middleware: Server] Extracted Secure Passport. Verified: { passport .is_verified } " )
112
103
else :
113
- logging . debug ("[Middleware: Server] No Secure Passport found or validation failed." )
104
+ print ("[Middleware: Server] No Secure Passport found or validation failed." )
114
105
115
- # next_handler is the agent's core task logic. We pass the message and the extracted passport.
116
106
return next_handler (message , passport )
107
+
108
+
0 commit comments