asap-protocol 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- asap/__init__.py +7 -0
- asap/cli.py +220 -0
- asap/errors.py +150 -0
- asap/examples/README.md +25 -0
- asap/examples/__init__.py +1 -0
- asap/examples/coordinator.py +184 -0
- asap/examples/echo_agent.py +100 -0
- asap/examples/run_demo.py +120 -0
- asap/models/__init__.py +146 -0
- asap/models/base.py +55 -0
- asap/models/constants.py +14 -0
- asap/models/entities.py +410 -0
- asap/models/enums.py +71 -0
- asap/models/envelope.py +94 -0
- asap/models/ids.py +55 -0
- asap/models/parts.py +207 -0
- asap/models/payloads.py +423 -0
- asap/models/types.py +39 -0
- asap/observability/__init__.py +43 -0
- asap/observability/logging.py +216 -0
- asap/observability/metrics.py +399 -0
- asap/schemas.py +203 -0
- asap/state/__init__.py +22 -0
- asap/state/machine.py +86 -0
- asap/state/snapshot.py +265 -0
- asap/transport/__init__.py +84 -0
- asap/transport/client.py +399 -0
- asap/transport/handlers.py +444 -0
- asap/transport/jsonrpc.py +190 -0
- asap/transport/middleware.py +359 -0
- asap/transport/server.py +739 -0
- asap_protocol-0.1.0.dist-info/METADATA +251 -0
- asap_protocol-0.1.0.dist-info/RECORD +36 -0
- asap_protocol-0.1.0.dist-info/WHEEL +4 -0
- asap_protocol-0.1.0.dist-info/entry_points.txt +2 -0
- asap_protocol-0.1.0.dist-info/licenses/LICENSE +190 -0
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
"""Authentication middleware for ASAP protocol server.
|
|
2
|
+
|
|
3
|
+
This module provides authentication middleware that:
|
|
4
|
+
- Validates Bearer tokens based on manifest configuration
|
|
5
|
+
- Verifies sender identity matches authenticated agent
|
|
6
|
+
- Supports custom token validation logic
|
|
7
|
+
- Returns proper JSON-RPC error responses for auth failures
|
|
8
|
+
|
|
9
|
+
Example:
|
|
10
|
+
>>> from asap.transport.middleware import AuthenticationMiddleware, BearerTokenValidator
|
|
11
|
+
>>> from asap.models.entities import Manifest, AuthScheme
|
|
12
|
+
>>>
|
|
13
|
+
>>> # Create manifest with auth configuration
|
|
14
|
+
>>> manifest = Manifest(
|
|
15
|
+
... id="urn:asap:agent:secure-agent",
|
|
16
|
+
... name="Secure Agent",
|
|
17
|
+
... version="1.0.0",
|
|
18
|
+
... description="Agent with authentication",
|
|
19
|
+
... auth=AuthScheme(schemes=["bearer"]),
|
|
20
|
+
... # ... other fields
|
|
21
|
+
>>> )
|
|
22
|
+
>>>
|
|
23
|
+
>>> # Create custom token validator
|
|
24
|
+
>>> def validate_token(token: str) -> str | None:
|
|
25
|
+
... # Validate token and return agent_id if valid
|
|
26
|
+
... if token == "valid-token-123":
|
|
27
|
+
... return "urn:asap:agent:authorized-client"
|
|
28
|
+
... return None
|
|
29
|
+
>>>
|
|
30
|
+
>>> validator = BearerTokenValidator(validate_token)
|
|
31
|
+
>>> middleware = AuthenticationMiddleware(manifest, validator)
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
import hashlib
|
|
35
|
+
from typing import Callable, Protocol
|
|
36
|
+
|
|
37
|
+
from fastapi import HTTPException, Request
|
|
38
|
+
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
|
39
|
+
|
|
40
|
+
from asap.models.entities import Manifest
|
|
41
|
+
from asap.observability import get_logger
|
|
42
|
+
|
|
43
|
+
logger = get_logger(__name__)
|
|
44
|
+
|
|
45
|
+
# Authentication header scheme
|
|
46
|
+
AUTH_SCHEME_BEARER = "bearer"
|
|
47
|
+
|
|
48
|
+
# HTTP status codes
|
|
49
|
+
HTTP_UNAUTHORIZED = 401
|
|
50
|
+
HTTP_FORBIDDEN = 403
|
|
51
|
+
|
|
52
|
+
# Error messages
|
|
53
|
+
ERROR_AUTH_REQUIRED = "Authentication required"
|
|
54
|
+
ERROR_INVALID_TOKEN = "Invalid authentication token"
|
|
55
|
+
ERROR_SENDER_MISMATCH = "Sender does not match authenticated identity"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class TokenValidator(Protocol):
|
|
59
|
+
"""Protocol for token validation implementations.
|
|
60
|
+
|
|
61
|
+
Custom validators must implement this interface to integrate
|
|
62
|
+
with the authentication middleware.
|
|
63
|
+
|
|
64
|
+
Example:
|
|
65
|
+
>>> class MyTokenValidator:
|
|
66
|
+
... def __call__(self, token: str) -> str | None:
|
|
67
|
+
... # Validate token against database, JWT, etc.
|
|
68
|
+
... if is_valid(token):
|
|
69
|
+
... return extract_agent_id(token)
|
|
70
|
+
... return None
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
def __call__(self, token: str) -> str | None:
|
|
74
|
+
"""Validate a token and return the authenticated agent ID.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
token: The authentication token to validate
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
The agent ID (URN) if token is valid, None otherwise
|
|
81
|
+
|
|
82
|
+
Example:
|
|
83
|
+
>>> validator = BearerTokenValidator(my_validate_func)
|
|
84
|
+
>>> agent_id = validator("token-123")
|
|
85
|
+
>>> print(agent_id) # "urn:asap:agent:client-1" or None
|
|
86
|
+
"""
|
|
87
|
+
...
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class BearerTokenValidator:
|
|
91
|
+
"""Default Bearer token validator implementation.
|
|
92
|
+
|
|
93
|
+
Wraps a validation function to conform to the TokenValidator protocol.
|
|
94
|
+
The validation function should take a token string and return an agent ID
|
|
95
|
+
if valid, or None if invalid.
|
|
96
|
+
|
|
97
|
+
Attributes:
|
|
98
|
+
validate_func: Function that validates tokens and returns agent IDs
|
|
99
|
+
|
|
100
|
+
Example:
|
|
101
|
+
>>> def my_validator(token: str) -> str | None:
|
|
102
|
+
... # Check token in database
|
|
103
|
+
... if token in valid_tokens:
|
|
104
|
+
... return valid_tokens[token]["agent_id"]
|
|
105
|
+
... return None
|
|
106
|
+
>>>
|
|
107
|
+
>>> validator = BearerTokenValidator(my_validator)
|
|
108
|
+
>>> agent_id = validator("abc123")
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
def __init__(self, validate_func: Callable[[str], str | None]) -> None:
|
|
112
|
+
"""Initialize the Bearer token validator.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
validate_func: Function that validates tokens and returns agent IDs
|
|
116
|
+
"""
|
|
117
|
+
self.validate_func = validate_func
|
|
118
|
+
|
|
119
|
+
def __call__(self, token: str) -> str | None:
|
|
120
|
+
"""Validate a token and return the authenticated agent ID.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
token: The authentication token to validate
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
The agent ID (URN) if token is valid, None otherwise
|
|
127
|
+
"""
|
|
128
|
+
return self.validate_func(token)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class AuthenticationMiddleware:
|
|
132
|
+
"""FastAPI middleware for ASAP protocol authentication.
|
|
133
|
+
|
|
134
|
+
This middleware handles authentication based on the manifest configuration:
|
|
135
|
+
- If manifest has no auth config, authentication is skipped
|
|
136
|
+
- If auth is configured, validates Bearer tokens
|
|
137
|
+
- Verifies sender in envelope matches authenticated identity
|
|
138
|
+
- Returns proper JSON-RPC error responses for failures
|
|
139
|
+
|
|
140
|
+
Attributes:
|
|
141
|
+
manifest: The agent manifest with auth configuration
|
|
142
|
+
validator: Token validator implementation
|
|
143
|
+
security: HTTPBearer security scheme from FastAPI
|
|
144
|
+
|
|
145
|
+
Example:
|
|
146
|
+
>>> from asap.transport.middleware import AuthenticationMiddleware
|
|
147
|
+
>>> from asap.models.entities import Manifest, AuthScheme
|
|
148
|
+
>>>
|
|
149
|
+
>>> manifest = Manifest(
|
|
150
|
+
... id="urn:asap:agent:my-agent",
|
|
151
|
+
... auth=AuthScheme(schemes=["bearer"]),
|
|
152
|
+
... # ... other fields
|
|
153
|
+
... )
|
|
154
|
+
>>>
|
|
155
|
+
>>> def validate_token(token: str) -> str | None:
|
|
156
|
+
... # Your validation logic
|
|
157
|
+
... return "urn:asap:agent:client" if valid else None
|
|
158
|
+
>>>
|
|
159
|
+
>>> validator = BearerTokenValidator(validate_token)
|
|
160
|
+
>>> middleware = AuthenticationMiddleware(manifest, validator)
|
|
161
|
+
"""
|
|
162
|
+
|
|
163
|
+
def __init__(
|
|
164
|
+
self,
|
|
165
|
+
manifest: Manifest,
|
|
166
|
+
validator: TokenValidator | None = None,
|
|
167
|
+
) -> None:
|
|
168
|
+
"""Initialize authentication middleware.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
manifest: Agent manifest with auth configuration
|
|
172
|
+
validator: Token validator implementation (optional if auth not required)
|
|
173
|
+
|
|
174
|
+
Raises:
|
|
175
|
+
ValueError: If manifest requires auth but no validator provided
|
|
176
|
+
"""
|
|
177
|
+
self.manifest = manifest
|
|
178
|
+
self.validator = validator
|
|
179
|
+
self.security = HTTPBearer(auto_error=False)
|
|
180
|
+
|
|
181
|
+
# Validate configuration
|
|
182
|
+
if self._is_auth_required() and validator is None:
|
|
183
|
+
raise ValueError(
|
|
184
|
+
"Token validator required when authentication is configured in manifest"
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
def _is_auth_required(self) -> bool:
|
|
188
|
+
"""Check if authentication is required by manifest.
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
True if manifest has auth configuration, False otherwise
|
|
192
|
+
"""
|
|
193
|
+
return self.manifest.auth is not None
|
|
194
|
+
|
|
195
|
+
def _supports_bearer_auth(self) -> bool:
|
|
196
|
+
"""Check if manifest supports Bearer token authentication.
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
True if "bearer" is in auth schemes, False otherwise
|
|
200
|
+
"""
|
|
201
|
+
if not self.manifest.auth:
|
|
202
|
+
return False
|
|
203
|
+
return AUTH_SCHEME_BEARER in self.manifest.auth.schemes
|
|
204
|
+
|
|
205
|
+
async def verify_authentication(
|
|
206
|
+
self,
|
|
207
|
+
request: Request,
|
|
208
|
+
credentials: HTTPAuthorizationCredentials | None = None,
|
|
209
|
+
) -> str | None:
|
|
210
|
+
"""Verify authentication for an incoming request.
|
|
211
|
+
|
|
212
|
+
This method:
|
|
213
|
+
1. Checks if authentication is required
|
|
214
|
+
2. Extracts credentials from Authorization header
|
|
215
|
+
3. Validates token using the configured validator
|
|
216
|
+
4. Returns authenticated agent ID or raises HTTPException
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
request: The incoming FastAPI request
|
|
220
|
+
credentials: Optional pre-extracted credentials
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
The authenticated agent ID (URN) if auth is required and valid,
|
|
224
|
+
None if auth is not required
|
|
225
|
+
|
|
226
|
+
Raises:
|
|
227
|
+
HTTPException: If authentication is required but fails (401 or 403)
|
|
228
|
+
|
|
229
|
+
Example:
|
|
230
|
+
>>> middleware = AuthenticationMiddleware(manifest, validator)
|
|
231
|
+
>>> agent_id = await middleware.verify_authentication(request)
|
|
232
|
+
>>> # agent_id is None if auth not required
|
|
233
|
+
>>> # agent_id is agent URN if auth successful
|
|
234
|
+
>>> # HTTPException raised if auth failed
|
|
235
|
+
"""
|
|
236
|
+
# Skip authentication if not required
|
|
237
|
+
if not self._is_auth_required():
|
|
238
|
+
logger.debug("asap.auth.skipped", reason="not_required_by_manifest")
|
|
239
|
+
return None
|
|
240
|
+
|
|
241
|
+
# Extract credentials if not provided
|
|
242
|
+
if credentials is None:
|
|
243
|
+
credentials = await self.security(request)
|
|
244
|
+
|
|
245
|
+
# Require credentials if auth is configured
|
|
246
|
+
if credentials is None:
|
|
247
|
+
logger.warning("asap.auth.missing", manifest_id=self.manifest.id)
|
|
248
|
+
raise HTTPException(
|
|
249
|
+
status_code=HTTP_UNAUTHORIZED,
|
|
250
|
+
detail=ERROR_AUTH_REQUIRED,
|
|
251
|
+
headers={"WWW-Authenticate": "Bearer"},
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
# Validate Authorization scheme (case-insensitive)
|
|
255
|
+
if credentials.scheme.lower() != AUTH_SCHEME_BEARER:
|
|
256
|
+
logger.warning(
|
|
257
|
+
"asap.auth.invalid_scheme",
|
|
258
|
+
manifest_id=self.manifest.id,
|
|
259
|
+
provided_scheme=credentials.scheme,
|
|
260
|
+
expected_scheme=AUTH_SCHEME_BEARER,
|
|
261
|
+
)
|
|
262
|
+
raise HTTPException(
|
|
263
|
+
status_code=HTTP_UNAUTHORIZED,
|
|
264
|
+
detail=ERROR_INVALID_TOKEN,
|
|
265
|
+
headers={"WWW-Authenticate": "Bearer"},
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
# Validate Bearer token support in manifest
|
|
269
|
+
if not self._supports_bearer_auth():
|
|
270
|
+
logger.warning(
|
|
271
|
+
"asap.auth.scheme_not_supported",
|
|
272
|
+
manifest_id=self.manifest.id,
|
|
273
|
+
provided_scheme=credentials.scheme,
|
|
274
|
+
)
|
|
275
|
+
raise HTTPException(
|
|
276
|
+
status_code=HTTP_UNAUTHORIZED,
|
|
277
|
+
detail=ERROR_INVALID_TOKEN,
|
|
278
|
+
headers={"WWW-Authenticate": "Bearer"},
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
# Validate token and get agent ID
|
|
282
|
+
token = credentials.credentials
|
|
283
|
+
# Type narrowing: validator is not None when auth is required (validated in __init__)
|
|
284
|
+
if self.validator is None:
|
|
285
|
+
raise RuntimeError(
|
|
286
|
+
"Token validator is None but authentication is required. "
|
|
287
|
+
"This should not happen if middleware was initialized correctly."
|
|
288
|
+
)
|
|
289
|
+
agent_id = self.validator(token)
|
|
290
|
+
|
|
291
|
+
if agent_id is None:
|
|
292
|
+
# Log token hash instead of prefix to avoid exposing token data
|
|
293
|
+
token_hash = hashlib.sha256(token.encode()).hexdigest()[:16]
|
|
294
|
+
logger.warning(
|
|
295
|
+
"asap.auth.invalid_token",
|
|
296
|
+
manifest_id=self.manifest.id,
|
|
297
|
+
token_hash=token_hash,
|
|
298
|
+
)
|
|
299
|
+
raise HTTPException(
|
|
300
|
+
status_code=HTTP_UNAUTHORIZED,
|
|
301
|
+
detail=ERROR_INVALID_TOKEN,
|
|
302
|
+
headers={"WWW-Authenticate": "Bearer"},
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
logger.info(
|
|
306
|
+
"asap.auth.success",
|
|
307
|
+
manifest_id=self.manifest.id,
|
|
308
|
+
authenticated_agent=agent_id,
|
|
309
|
+
)
|
|
310
|
+
return agent_id
|
|
311
|
+
|
|
312
|
+
def verify_sender_matches_auth(
|
|
313
|
+
self,
|
|
314
|
+
authenticated_agent_id: str | None,
|
|
315
|
+
envelope_sender: str,
|
|
316
|
+
) -> None:
|
|
317
|
+
"""Verify that envelope sender matches authenticated identity.
|
|
318
|
+
|
|
319
|
+
This prevents agents from spoofing the sender field in the envelope.
|
|
320
|
+
Only enforced when authentication is enabled.
|
|
321
|
+
|
|
322
|
+
Args:
|
|
323
|
+
authenticated_agent_id: The agent ID from authentication, or None
|
|
324
|
+
envelope_sender: The sender field from the envelope
|
|
325
|
+
|
|
326
|
+
Raises:
|
|
327
|
+
HTTPException: If sender doesn't match authenticated identity (403)
|
|
328
|
+
|
|
329
|
+
Example:
|
|
330
|
+
>>> middleware.verify_sender_matches_auth(
|
|
331
|
+
... authenticated_agent_id="urn:asap:agent:client-1",
|
|
332
|
+
... envelope_sender="urn:asap:agent:client-1"
|
|
333
|
+
... ) # OK
|
|
334
|
+
>>>
|
|
335
|
+
>>> middleware.verify_sender_matches_auth(
|
|
336
|
+
... authenticated_agent_id="urn:asap:agent:client-1",
|
|
337
|
+
... envelope_sender="urn:asap:agent:spoofed"
|
|
338
|
+
... ) # Raises HTTPException
|
|
339
|
+
"""
|
|
340
|
+
# Skip verification if auth is not enabled
|
|
341
|
+
if authenticated_agent_id is None:
|
|
342
|
+
return
|
|
343
|
+
|
|
344
|
+
# Verify sender matches authenticated identity
|
|
345
|
+
if envelope_sender != authenticated_agent_id:
|
|
346
|
+
logger.warning(
|
|
347
|
+
"asap.auth.sender_mismatch",
|
|
348
|
+
authenticated_agent=authenticated_agent_id,
|
|
349
|
+
envelope_sender=envelope_sender,
|
|
350
|
+
)
|
|
351
|
+
raise HTTPException(
|
|
352
|
+
status_code=HTTP_FORBIDDEN,
|
|
353
|
+
detail=ERROR_SENDER_MISMATCH,
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
logger.debug(
|
|
357
|
+
"asap.auth.sender_verified",
|
|
358
|
+
authenticated_agent=authenticated_agent_id,
|
|
359
|
+
)
|