asap-protocol 0.5.0__py3-none-any.whl → 1.0.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 +1 -1
- asap/cli.py +137 -2
- asap/examples/README.md +81 -13
- asap/examples/auth_patterns.py +212 -0
- asap/examples/error_recovery.py +248 -0
- asap/examples/long_running.py +287 -0
- asap/examples/mcp_integration.py +240 -0
- asap/examples/multi_step_workflow.py +134 -0
- asap/examples/orchestration.py +293 -0
- asap/examples/rate_limiting.py +137 -0
- asap/examples/run_demo.py +0 -2
- asap/examples/secure_handler.py +84 -0
- asap/examples/state_migration.py +240 -0
- asap/examples/streaming_response.py +108 -0
- asap/examples/websocket_concept.py +129 -0
- asap/mcp/__init__.py +43 -0
- asap/mcp/client.py +224 -0
- asap/mcp/protocol.py +179 -0
- asap/mcp/server.py +333 -0
- asap/mcp/server_runner.py +40 -0
- asap/models/base.py +0 -3
- asap/models/constants.py +3 -1
- asap/models/entities.py +21 -6
- asap/models/envelope.py +7 -0
- asap/models/ids.py +8 -4
- asap/models/parts.py +33 -3
- asap/models/validators.py +16 -0
- asap/observability/__init__.py +6 -0
- asap/observability/dashboards/README.md +24 -0
- asap/observability/dashboards/asap-detailed.json +131 -0
- asap/observability/dashboards/asap-red.json +129 -0
- asap/observability/logging.py +81 -1
- asap/observability/metrics.py +15 -1
- asap/observability/trace_parser.py +238 -0
- asap/observability/trace_ui.py +218 -0
- asap/observability/tracing.py +293 -0
- asap/state/machine.py +15 -2
- asap/state/snapshot.py +0 -9
- asap/testing/__init__.py +31 -0
- asap/testing/assertions.py +108 -0
- asap/testing/fixtures.py +113 -0
- asap/testing/mocks.py +152 -0
- asap/transport/__init__.py +28 -0
- asap/transport/cache.py +180 -0
- asap/transport/circuit_breaker.py +9 -8
- asap/transport/client.py +418 -36
- asap/transport/compression.py +389 -0
- asap/transport/handlers.py +106 -53
- asap/transport/middleware.py +58 -34
- asap/transport/server.py +429 -139
- asap/transport/validators.py +0 -4
- asap/utils/sanitization.py +0 -5
- asap_protocol-1.0.0.dist-info/METADATA +264 -0
- asap_protocol-1.0.0.dist-info/RECORD +70 -0
- asap_protocol-0.5.0.dist-info/METADATA +0 -244
- asap_protocol-0.5.0.dist-info/RECORD +0 -41
- {asap_protocol-0.5.0.dist-info → asap_protocol-1.0.0.dist-info}/WHEEL +0 -0
- {asap_protocol-0.5.0.dist-info → asap_protocol-1.0.0.dist-info}/entry_points.txt +0 -0
- {asap_protocol-0.5.0.dist-info → asap_protocol-1.0.0.dist-info}/licenses/LICENSE +0 -0
asap/transport/middleware.py
CHANGED
|
@@ -32,8 +32,10 @@ Example:
|
|
|
32
32
|
>>> middleware = AuthenticationMiddleware(manifest, validator)
|
|
33
33
|
"""
|
|
34
34
|
|
|
35
|
+
import asyncio
|
|
36
|
+
import inspect
|
|
35
37
|
import uuid
|
|
36
|
-
from typing import Any, Awaitable, Callable, Protocol
|
|
38
|
+
from typing import Any, Awaitable, Callable, Protocol, cast
|
|
37
39
|
from collections.abc import Sequence
|
|
38
40
|
|
|
39
41
|
from fastapi import HTTPException, Request
|
|
@@ -54,7 +56,9 @@ logger = get_logger(__name__)
|
|
|
54
56
|
AUTH_SCHEME_BEARER = "bearer"
|
|
55
57
|
|
|
56
58
|
# Rate limiting default configuration
|
|
57
|
-
|
|
59
|
+
# Uses token bucket pattern: burst limit (per second) + sustained limit (per minute)
|
|
60
|
+
# This allows short bursts while preventing sustained abuse
|
|
61
|
+
DEFAULT_RATE_LIMIT = "10/second;100/minute"
|
|
58
62
|
|
|
59
63
|
|
|
60
64
|
def _get_sender_from_envelope(request: Request) -> str:
|
|
@@ -80,9 +84,7 @@ def _get_sender_from_envelope(request: Request) -> str:
|
|
|
80
84
|
>>> sender = _get_sender_from_envelope(request)
|
|
81
85
|
>>> # Returns "192.168.1.1" (IP address, not sender URN)
|
|
82
86
|
"""
|
|
83
|
-
# Try to extract sender from envelope if already parsed (early returns reduce complexity)
|
|
84
87
|
try:
|
|
85
|
-
# Check if envelope is stored in request state (after parsing)
|
|
86
88
|
if hasattr(request.state, "envelope") and request.state.envelope:
|
|
87
89
|
envelope = request.state.envelope
|
|
88
90
|
if hasattr(envelope, "sender") and isinstance(envelope.sender, str):
|
|
@@ -105,17 +107,12 @@ def _get_sender_from_envelope(request: Request) -> str:
|
|
|
105
107
|
# Envelope not available, fall back to IP
|
|
106
108
|
pass
|
|
107
109
|
|
|
108
|
-
# Fallback to client IP address
|
|
109
110
|
remote_addr = get_remote_address(request)
|
|
110
|
-
# Type narrowing: get_remote_address returns str, but mypy may see it as Any
|
|
111
111
|
if isinstance(remote_addr, str):
|
|
112
112
|
return remote_addr
|
|
113
113
|
return str(remote_addr)
|
|
114
114
|
|
|
115
115
|
|
|
116
|
-
# Create rate limiter instance with IP-based key function
|
|
117
|
-
# Note: The key function attempts to extract sender but always falls back to IP
|
|
118
|
-
# because rate limiting executes before request body parsing
|
|
119
116
|
limiter = Limiter(
|
|
120
117
|
key_func=_get_sender_from_envelope,
|
|
121
118
|
default_limits=[DEFAULT_RATE_LIMIT],
|
|
@@ -154,8 +151,15 @@ def create_test_limiter(limits: Sequence[str] | None = None) -> Limiter:
|
|
|
154
151
|
def create_limiter(limits: Sequence[str] | None = None) -> Limiter:
|
|
155
152
|
"""Create a new limiter instance for production use.
|
|
156
153
|
|
|
157
|
-
Creates an isolated limiter instance with its own storage
|
|
158
|
-
multiple FastAPI app instances to have
|
|
154
|
+
Creates an isolated limiter instance with its own in-memory storage
|
|
155
|
+
(``memory://``), allowing multiple FastAPI app instances to have
|
|
156
|
+
independent rate limiters.
|
|
157
|
+
|
|
158
|
+
**Multi-worker warning:** ``memory://`` storage is per-process. In
|
|
159
|
+
multi-worker deployments (e.g., Gunicorn with 4 workers), each worker
|
|
160
|
+
has isolated limits—effective rate = configured limit × number of workers
|
|
161
|
+
(e.g., 10/s → 40/s across workers). For shared limits in production,
|
|
162
|
+
use Redis-backed storage via slowapi's ``storage_uri``.
|
|
159
163
|
|
|
160
164
|
Args:
|
|
161
165
|
limits: Optional list of rate limit strings (e.g., ["100/minute"]).
|
|
@@ -173,10 +177,16 @@ def create_limiter(limits: Sequence[str] | None = None) -> Limiter:
|
|
|
173
177
|
|
|
174
178
|
# Use unique storage URI to ensure isolation between app instances
|
|
175
179
|
unique_storage_id = str(uuid.uuid4())
|
|
180
|
+
storage_uri = f"memory://{unique_storage_id}"
|
|
181
|
+
logger.warning(
|
|
182
|
+
"asap.rate_limit.memory_storage",
|
|
183
|
+
message="memory:// storage is per-process; in multi-worker deployments "
|
|
184
|
+
"(e.g., Gunicorn), effective rate = limit × workers. Use Redis for shared limits.",
|
|
185
|
+
)
|
|
176
186
|
return Limiter(
|
|
177
187
|
key_func=_get_sender_from_envelope,
|
|
178
188
|
default_limits=list(limits),
|
|
179
|
-
storage_uri=
|
|
189
|
+
storage_uri=storage_uri,
|
|
180
190
|
)
|
|
181
191
|
|
|
182
192
|
|
|
@@ -197,7 +207,6 @@ def rate_limit_handler(request: Request, exc: Exception) -> JSONResponse:
|
|
|
197
207
|
>>> response = rate_limit_handler(request, exc)
|
|
198
208
|
>>> # Returns JSONResponse with status_code=429 and JSON-RPC error
|
|
199
209
|
"""
|
|
200
|
-
# Type narrowing: FastAPI passes RateLimitExceeded but handler signature uses Exception
|
|
201
210
|
if not isinstance(exc, RateLimitExceeded):
|
|
202
211
|
# Fallback for unexpected exception types
|
|
203
212
|
logger.warning("asap.rate_limit.unexpected_exception", exc_type=type(exc).__name__)
|
|
@@ -269,25 +278,32 @@ class TokenValidator(Protocol):
|
|
|
269
278
|
"""Protocol for token validation implementations.
|
|
270
279
|
|
|
271
280
|
Custom validators must implement this interface to integrate
|
|
272
|
-
with the authentication middleware.
|
|
281
|
+
with the authentication middleware. Validators may be sync or async.
|
|
282
|
+
Sync validators that perform I/O (e.g., DB/Redis lookups) are run
|
|
283
|
+
in a thread pool to avoid blocking the event loop.
|
|
273
284
|
|
|
274
|
-
Example:
|
|
275
|
-
>>> class
|
|
285
|
+
Example (sync):
|
|
286
|
+
>>> class MySyncValidator:
|
|
276
287
|
... def __call__(self, token: str) -> str | None:
|
|
277
|
-
... # Validate token against database, JWT, etc.
|
|
278
288
|
... if is_valid(token):
|
|
279
289
|
... return extract_agent_id(token)
|
|
280
290
|
... return None
|
|
291
|
+
|
|
292
|
+
Example (async):
|
|
293
|
+
>>> class MyAsyncValidator:
|
|
294
|
+
... async def __call__(self, token: str) -> str | None:
|
|
295
|
+
... return await db.lookup_agent(token)
|
|
281
296
|
"""
|
|
282
297
|
|
|
283
|
-
def __call__(self, token: str) -> str | None:
|
|
298
|
+
def __call__(self, token: str) -> str | None | Awaitable[str | None]:
|
|
284
299
|
"""Validate a token and return the authenticated agent ID.
|
|
285
300
|
|
|
286
301
|
Args:
|
|
287
302
|
token: The authentication token to validate
|
|
288
303
|
|
|
289
304
|
Returns:
|
|
290
|
-
The agent ID (URN) if token is valid, None otherwise
|
|
305
|
+
The agent ID (URN) if token is valid, None otherwise.
|
|
306
|
+
May return a coroutine for async validators.
|
|
291
307
|
|
|
292
308
|
Example:
|
|
293
309
|
>>> validator = BearerTokenValidator(my_validate_func)
|
|
@@ -301,15 +317,14 @@ class BearerTokenValidator:
|
|
|
301
317
|
"""Default Bearer token validator implementation.
|
|
302
318
|
|
|
303
319
|
Wraps a validation function to conform to the TokenValidator protocol.
|
|
304
|
-
The validation function
|
|
305
|
-
|
|
320
|
+
The validation function may be sync or async. Sync validators that perform
|
|
321
|
+
I/O are run in a thread pool by the middleware to avoid blocking the loop.
|
|
306
322
|
|
|
307
323
|
Attributes:
|
|
308
324
|
validate_func: Function that validates tokens and returns agent IDs
|
|
309
325
|
|
|
310
326
|
Example:
|
|
311
327
|
>>> def my_validator(token: str) -> str | None:
|
|
312
|
-
... # Check token in database
|
|
313
328
|
... if token in valid_tokens:
|
|
314
329
|
... return valid_tokens[token]["agent_id"]
|
|
315
330
|
... return None
|
|
@@ -318,22 +333,28 @@ class BearerTokenValidator:
|
|
|
318
333
|
>>> agent_id = validator("abc123")
|
|
319
334
|
"""
|
|
320
335
|
|
|
321
|
-
def __init__(
|
|
336
|
+
def __init__(
|
|
337
|
+
self,
|
|
338
|
+
validate_func: Callable[[str], str | None | Awaitable[str | None]],
|
|
339
|
+
) -> None:
|
|
322
340
|
"""Initialize the Bearer token validator.
|
|
323
341
|
|
|
324
342
|
Args:
|
|
325
|
-
validate_func:
|
|
343
|
+
validate_func: Sync or async function that validates tokens
|
|
344
|
+
and returns agent IDs. Sync functions with I/O are run
|
|
345
|
+
in a thread pool to avoid blocking the event loop.
|
|
326
346
|
"""
|
|
327
347
|
self.validate_func = validate_func
|
|
328
348
|
|
|
329
|
-
def __call__(self, token: str) -> str | None:
|
|
349
|
+
def __call__(self, token: str) -> str | None | Awaitable[str | None]:
|
|
330
350
|
"""Validate a token and return the authenticated agent ID.
|
|
331
351
|
|
|
332
352
|
Args:
|
|
333
353
|
token: The authentication token to validate
|
|
334
354
|
|
|
335
355
|
Returns:
|
|
336
|
-
The agent ID (URN) if token is valid, None otherwise
|
|
356
|
+
The agent ID (URN) if token is valid, None otherwise.
|
|
357
|
+
May return a coroutine for async validate_func.
|
|
337
358
|
"""
|
|
338
359
|
return self.validate_func(token)
|
|
339
360
|
|
|
@@ -388,7 +409,6 @@ class AuthenticationMiddleware:
|
|
|
388
409
|
self.validator = validator
|
|
389
410
|
self.security = HTTPBearer(auto_error=False)
|
|
390
411
|
|
|
391
|
-
# Validate configuration
|
|
392
412
|
if self._is_auth_required() and validator is None:
|
|
393
413
|
raise ValueError(
|
|
394
414
|
"Token validator required when authentication is configured in manifest"
|
|
@@ -461,7 +481,6 @@ class AuthenticationMiddleware:
|
|
|
461
481
|
headers={"WWW-Authenticate": "Bearer"},
|
|
462
482
|
)
|
|
463
483
|
|
|
464
|
-
# Validate Authorization scheme (case-insensitive)
|
|
465
484
|
if credentials.scheme.lower() != AUTH_SCHEME_BEARER:
|
|
466
485
|
logger.warning(
|
|
467
486
|
"asap.auth.invalid_scheme",
|
|
@@ -475,7 +494,6 @@ class AuthenticationMiddleware:
|
|
|
475
494
|
headers={"WWW-Authenticate": "Bearer"},
|
|
476
495
|
)
|
|
477
496
|
|
|
478
|
-
# Validate Bearer token support in manifest
|
|
479
497
|
if not self._supports_bearer_auth():
|
|
480
498
|
logger.warning(
|
|
481
499
|
"asap.auth.scheme_not_supported",
|
|
@@ -488,15 +506,23 @@ class AuthenticationMiddleware:
|
|
|
488
506
|
headers={"WWW-Authenticate": "Bearer"},
|
|
489
507
|
)
|
|
490
508
|
|
|
491
|
-
# Validate token and get agent ID
|
|
492
509
|
token = credentials.credentials
|
|
493
|
-
# Type narrowing: validator is not None when auth is required (validated in __init__)
|
|
494
510
|
if self.validator is None:
|
|
495
511
|
raise RuntimeError(
|
|
496
512
|
"Token validator is None but authentication is required. "
|
|
497
513
|
"This should not happen if middleware was initialized correctly."
|
|
498
514
|
)
|
|
499
|
-
|
|
515
|
+
if inspect.iscoroutinefunction(self.validator.__call__):
|
|
516
|
+
result = self.validator(token)
|
|
517
|
+
agent_id = await cast(Awaitable[str | None], result)
|
|
518
|
+
else:
|
|
519
|
+
# Cast: to_thread expects Callable[..., T]; TokenValidator may return Awaitable
|
|
520
|
+
# but we handle that below (BearerTokenValidator wrapping async func)
|
|
521
|
+
agent_id = await asyncio.to_thread(
|
|
522
|
+
cast("Callable[[str], str | None]", self.validator), token
|
|
523
|
+
)
|
|
524
|
+
if inspect.isawaitable(agent_id):
|
|
525
|
+
agent_id = await cast(Awaitable[str | None], agent_id)
|
|
500
526
|
|
|
501
527
|
if agent_id is None:
|
|
502
528
|
# Log sanitized token to avoid exposing full token data
|
|
@@ -615,7 +641,6 @@ class SizeLimitMiddleware(BaseHTTPMiddleware):
|
|
|
615
641
|
Returns:
|
|
616
642
|
Response from next handler or error response if size exceeded
|
|
617
643
|
"""
|
|
618
|
-
# Check Content-Length header if present
|
|
619
644
|
content_length = request.headers.get("content-length")
|
|
620
645
|
if content_length:
|
|
621
646
|
try:
|
|
@@ -634,7 +659,6 @@ class SizeLimitMiddleware(BaseHTTPMiddleware):
|
|
|
634
659
|
},
|
|
635
660
|
)
|
|
636
661
|
except ValueError:
|
|
637
|
-
# Invalid Content-Length header, let route handler validate actual body size
|
|
638
662
|
pass
|
|
639
663
|
|
|
640
664
|
# Continue to next middleware or route handler
|