asap-protocol 0.3.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.
Files changed (62) hide show
  1. asap/__init__.py +1 -1
  2. asap/cli.py +137 -2
  3. asap/errors.py +167 -0
  4. asap/examples/README.md +81 -10
  5. asap/examples/auth_patterns.py +212 -0
  6. asap/examples/error_recovery.py +248 -0
  7. asap/examples/long_running.py +287 -0
  8. asap/examples/mcp_integration.py +240 -0
  9. asap/examples/multi_step_workflow.py +134 -0
  10. asap/examples/orchestration.py +293 -0
  11. asap/examples/rate_limiting.py +137 -0
  12. asap/examples/run_demo.py +9 -4
  13. asap/examples/secure_handler.py +84 -0
  14. asap/examples/state_migration.py +240 -0
  15. asap/examples/streaming_response.py +108 -0
  16. asap/examples/websocket_concept.py +129 -0
  17. asap/mcp/__init__.py +43 -0
  18. asap/mcp/client.py +224 -0
  19. asap/mcp/protocol.py +179 -0
  20. asap/mcp/server.py +333 -0
  21. asap/mcp/server_runner.py +40 -0
  22. asap/models/__init__.py +4 -0
  23. asap/models/base.py +0 -3
  24. asap/models/constants.py +76 -1
  25. asap/models/entities.py +58 -7
  26. asap/models/envelope.py +14 -1
  27. asap/models/ids.py +8 -4
  28. asap/models/parts.py +33 -3
  29. asap/models/validators.py +16 -0
  30. asap/observability/__init__.py +6 -0
  31. asap/observability/dashboards/README.md +24 -0
  32. asap/observability/dashboards/asap-detailed.json +131 -0
  33. asap/observability/dashboards/asap-red.json +129 -0
  34. asap/observability/logging.py +81 -1
  35. asap/observability/metrics.py +15 -1
  36. asap/observability/trace_parser.py +238 -0
  37. asap/observability/trace_ui.py +218 -0
  38. asap/observability/tracing.py +293 -0
  39. asap/state/machine.py +15 -2
  40. asap/state/snapshot.py +0 -9
  41. asap/testing/__init__.py +31 -0
  42. asap/testing/assertions.py +108 -0
  43. asap/testing/fixtures.py +113 -0
  44. asap/testing/mocks.py +152 -0
  45. asap/transport/__init__.py +31 -0
  46. asap/transport/cache.py +180 -0
  47. asap/transport/circuit_breaker.py +194 -0
  48. asap/transport/client.py +989 -72
  49. asap/transport/compression.py +389 -0
  50. asap/transport/handlers.py +106 -53
  51. asap/transport/middleware.py +64 -39
  52. asap/transport/server.py +461 -94
  53. asap/transport/validators.py +320 -0
  54. asap/utils/__init__.py +7 -0
  55. asap/utils/sanitization.py +134 -0
  56. asap_protocol-1.0.0.dist-info/METADATA +264 -0
  57. asap_protocol-1.0.0.dist-info/RECORD +70 -0
  58. asap_protocol-0.3.0.dist-info/METADATA +0 -227
  59. asap_protocol-0.3.0.dist-info/RECORD +0 -37
  60. {asap_protocol-0.3.0.dist-info → asap_protocol-1.0.0.dist-info}/WHEEL +0 -0
  61. {asap_protocol-0.3.0.dist-info → asap_protocol-1.0.0.dist-info}/entry_points.txt +0 -0
  62. {asap_protocol-0.3.0.dist-info → asap_protocol-1.0.0.dist-info}/licenses/LICENSE +0 -0
asap/__init__.py CHANGED
@@ -4,4 +4,4 @@ A streamlined, scalable, asynchronous protocol for agent-to-agent communication
4
4
  and task coordination.
5
5
  """
6
6
 
7
- __version__ = "0.3.0"
7
+ __version__ = "1.0.0"
asap/cli.py CHANGED
@@ -1,6 +1,7 @@
1
1
  """Command-line interface for ASAP Protocol utilities.
2
2
 
3
- This module provides CLI commands for schema export, inspection, and validation.
3
+ This module provides CLI commands for schema export, inspection, validation,
4
+ trace visualization, and an interactive REPL for testing payloads.
4
5
 
5
6
  Example:
6
7
  >>> # From terminal:
@@ -9,9 +10,14 @@ Example:
9
10
  >>> # asap list-schemas
10
11
  >>> # asap show-schema agent
11
12
  >>> # asap validate-schema message.json --schema-type envelope
13
+ >>> # asap trace <trace-id> [--log-file asap.log] [--format ascii|json]
14
+ >>> # asap repl # Interactive REPL with ASAP models
12
15
  """
13
16
 
17
+ import code
14
18
  import json
19
+ import os
20
+ import sys
15
21
  from pathlib import Path
16
22
  from typing import Annotated, Optional
17
23
 
@@ -19,6 +25,10 @@ import typer
19
25
  from pydantic import ValidationError
20
26
 
21
27
  from asap import __version__
28
+ from asap.models import Envelope, TaskRequest, generate_id
29
+ from asap.models.entities import Capability, Endpoint, Manifest, Skill
30
+
31
+ from asap.observability.trace_parser import parse_trace_from_lines, trace_to_json_export
22
32
  from asap.schemas import SCHEMA_REGISTRY, export_all_schemas, get_schema_json, list_schema_entries
23
33
 
24
34
  app = typer.Typer(help="ASAP Protocol CLI.")
@@ -175,7 +185,6 @@ def validate_schema(
175
185
  message_send, state_query, state_restore, artifact_notify, mcp_tool_call,
176
186
  mcp_tool_result, mcp_resource_fetch, mcp_resource_data, envelope.
177
187
  """
178
- # Check file exists
179
188
  if not file.exists():
180
189
  raise typer.BadParameter(f"File not found: {file}")
181
190
 
@@ -211,6 +220,132 @@ def validate_schema(
211
220
  typer.echo(f"Valid {effective_schema_type} schema: {file}")
212
221
 
213
222
 
223
+ # Environment variable for default trace log file
224
+ ENV_TRACE_LOG = "ASAP_TRACE_LOG"
225
+
226
+ # REPL banner and namespace
227
+ REPL_BANNER = (
228
+ "ASAP Protocol REPL - test payloads interactively.\n"
229
+ " Envelope, TaskRequest, Manifest, generate_id, sample_envelope() available.\n"
230
+ " Type exit() or Ctrl-D to quit."
231
+ )
232
+
233
+
234
+ def _repl_namespace() -> dict[str, object]:
235
+ """Build namespace for the ASAP REPL with models and a sample envelope helper."""
236
+
237
+ def sample_envelope() -> Envelope:
238
+ """Return a sample task.request envelope for quick testing."""
239
+ return Envelope(
240
+ asap_version="0.1",
241
+ sender="urn:asap:agent:repl-sender",
242
+ recipient="urn:asap:agent:repl-recipient",
243
+ payload_type="task.request",
244
+ payload=TaskRequest(
245
+ conversation_id=f"conv-{generate_id()}",
246
+ skill_id="echo",
247
+ input={"message": "hello from REPL"},
248
+ ).model_dump(),
249
+ )
250
+
251
+ return {
252
+ "Envelope": Envelope,
253
+ "TaskRequest": TaskRequest,
254
+ "Manifest": Manifest,
255
+ "Capability": Capability,
256
+ "Endpoint": Endpoint,
257
+ "Skill": Skill,
258
+ "generate_id": generate_id,
259
+ "sample_envelope": sample_envelope,
260
+ }
261
+
262
+
263
+ @app.command("repl")
264
+ def repl() -> None:
265
+ """Start an interactive REPL with ASAP models for testing payloads.
266
+
267
+ Provides Envelope, TaskRequest, Manifest, generate_id, and sample_envelope()
268
+ in the namespace. Use Python's code module for the interactive loop.
269
+ """
270
+ namespace = _repl_namespace()
271
+ code.interact(banner=REPL_BANNER, local=namespace)
272
+
273
+
274
+ @app.command("trace")
275
+ def trace(
276
+ trace_id: Annotated[
277
+ Optional[str],
278
+ typer.Argument(help="Trace ID to search for in logs (e.g. from envelope.trace_id)."),
279
+ ] = None,
280
+ log_file: Annotated[
281
+ Optional[Path],
282
+ typer.Option(
283
+ "--log-file",
284
+ "-f",
285
+ help="Log file to search (JSON lines). Default: ASAP_TRACE_LOG env or stdin.",
286
+ ),
287
+ ] = None,
288
+ output_format: Annotated[
289
+ str,
290
+ typer.Option(
291
+ "--format",
292
+ "-o",
293
+ help="Output format: ascii (diagram) or json (for external tools).",
294
+ ),
295
+ ] = "ascii",
296
+ ) -> None:
297
+ """Show request flow and timing for a trace ID from ASAP JSON logs.
298
+
299
+ Searches log lines for asap.request.received and asap.request.processed
300
+ events with the given trace_id and prints an ASCII diagram of the flow
301
+ with latency per hop (e.g. agent_a -> agent_b (15ms) -> agent_c (23ms)).
302
+
303
+ Use --format json to output structured JSON for piping to jq, CI, or
304
+ observability platforms.
305
+
306
+ Logs must be JSON lines (ASAP_LOG_FORMAT=json). Use --log-file to pass
307
+ a file, or set ASAP_TRACE_LOG; otherwise reads from stdin.
308
+ """
309
+ effective_log_file = log_file
310
+ if effective_log_file is None and os.environ.get(ENV_TRACE_LOG):
311
+ effective_log_file = Path(os.environ[ENV_TRACE_LOG])
312
+
313
+ if trace_id is None or trace_id.strip() == "":
314
+ typer.echo(
315
+ "Error: trace_id is required. Usage: asap trace <trace-id> [--log-file PATH]", err=True
316
+ )
317
+ raise typer.Exit(1)
318
+
319
+ trace_id = trace_id.strip()
320
+ fmt = output_format.strip().lower() if output_format else "ascii"
321
+ if fmt not in ("ascii", "json"):
322
+ typer.echo("Error: --format must be 'ascii' or 'json'", err=True)
323
+ raise typer.Exit(1)
324
+
325
+ def _lines() -> list[str]:
326
+ if effective_log_file is None:
327
+ return sys.stdin.readlines()
328
+ if not effective_log_file.exists():
329
+ raise typer.BadParameter(f"Log file not found: {effective_log_file}")
330
+ return effective_log_file.read_text(encoding="utf-8").splitlines()
331
+
332
+ try:
333
+ lines = _lines()
334
+ except typer.BadParameter:
335
+ raise
336
+ except OSError as exc:
337
+ raise typer.BadParameter(f"Cannot read log file: {exc}") from exc
338
+
339
+ hops, diagram = parse_trace_from_lines(lines, trace_id)
340
+ if not hops:
341
+ typer.echo(f"No trace found for: {trace_id}")
342
+ raise typer.Exit(1)
343
+ if fmt == "json":
344
+ typer.echo(json.dumps(trace_to_json_export(trace_id, hops), indent=2))
345
+ else:
346
+ typer.echo(diagram)
347
+
348
+
214
349
  def main() -> None:
215
350
  """Run the ASAP Protocol CLI."""
216
351
  app()
asap/errors.py CHANGED
@@ -190,3 +190,170 @@ class ThreadPoolExhaustedError(ASAPError):
190
190
  )
191
191
  self.max_threads = max_threads
192
192
  self.active_threads = active_threads
193
+
194
+
195
+ class InvalidTimestampError(ASAPError):
196
+ """Raised when an envelope timestamp is invalid (too old or too far in the future).
197
+
198
+ This error occurs when validating envelope timestamps for replay attack prevention.
199
+ Envelopes with timestamps outside the acceptable window are rejected.
200
+
201
+ Attributes:
202
+ timestamp: The invalid timestamp value
203
+ age_seconds: Age of the envelope in seconds (if too old)
204
+ future_offset_seconds: Offset in seconds from current time (if too far in future)
205
+ """
206
+
207
+ def __init__(
208
+ self,
209
+ timestamp: str,
210
+ message: str,
211
+ age_seconds: float | None = None,
212
+ future_offset_seconds: float | None = None,
213
+ details: dict[str, Any] | None = None,
214
+ ) -> None:
215
+ """Initialize invalid timestamp error.
216
+
217
+ Args:
218
+ timestamp: The invalid timestamp value
219
+ message: Human-readable error description
220
+ age_seconds: Age of the envelope in seconds (if too old)
221
+ future_offset_seconds: Offset in seconds from current time (if too far in future)
222
+ details: Optional additional context
223
+ """
224
+ # Build details dict with optional fields
225
+ details_dict: dict[str, Any] = {"timestamp": timestamp}
226
+ if age_seconds is not None:
227
+ details_dict["age_seconds"] = age_seconds
228
+ if future_offset_seconds is not None:
229
+ details_dict["future_offset_seconds"] = future_offset_seconds
230
+ if details:
231
+ details_dict.update(details)
232
+
233
+ super().__init__(
234
+ code="asap:protocol/invalid_timestamp",
235
+ message=message,
236
+ details=details_dict,
237
+ )
238
+ self.timestamp = timestamp
239
+ self.age_seconds = age_seconds
240
+ self.future_offset_seconds = future_offset_seconds
241
+
242
+
243
+ class InvalidNonceError(ASAPError):
244
+ """Raised when an envelope nonce is invalid (duplicate or malformed).
245
+
246
+ This error occurs when validating envelope nonces for replay attack prevention.
247
+ Nonces that have been used before within the TTL window are rejected.
248
+
249
+ Attributes:
250
+ nonce: The invalid nonce value
251
+ """
252
+
253
+ def __init__(
254
+ self,
255
+ nonce: str,
256
+ message: str,
257
+ details: dict[str, Any] | None = None,
258
+ ) -> None:
259
+ """Initialize invalid nonce error.
260
+
261
+ Args:
262
+ nonce: The invalid nonce value
263
+ message: Human-readable error description
264
+ details: Optional additional context
265
+ """
266
+ super().__init__(
267
+ code="asap:protocol/invalid_nonce",
268
+ message=message,
269
+ details={
270
+ "nonce": nonce,
271
+ **(details or {}),
272
+ },
273
+ )
274
+ self.nonce = nonce
275
+
276
+
277
+ class CircuitOpenError(ASAPError):
278
+ """Raised when circuit breaker is open and request is rejected.
279
+
280
+ This error occurs when the circuit breaker pattern has detected
281
+ too many consecutive failures and is preventing further requests
282
+ to protect the system from cascading failures.
283
+
284
+ Attributes:
285
+ base_url: The URL for which the circuit is open
286
+ consecutive_failures: Number of consecutive failures that opened the circuit
287
+ """
288
+
289
+ def __init__(
290
+ self,
291
+ base_url: str,
292
+ consecutive_failures: int,
293
+ details: dict[str, Any] | None = None,
294
+ ) -> None:
295
+ """Initialize circuit open error.
296
+
297
+ Args:
298
+ base_url: The URL for which the circuit is open
299
+ consecutive_failures: Number of consecutive failures
300
+ details: Optional additional context
301
+ """
302
+ message = (
303
+ f"Circuit breaker is OPEN for {base_url}. "
304
+ f"Too many consecutive failures ({consecutive_failures}). "
305
+ "Service temporarily unavailable."
306
+ )
307
+ super().__init__(
308
+ code="asap:transport/circuit_open",
309
+ message=message,
310
+ details={
311
+ "base_url": base_url,
312
+ "consecutive_failures": consecutive_failures,
313
+ **(details or {}),
314
+ },
315
+ )
316
+ self.base_url = base_url
317
+ self.consecutive_failures = consecutive_failures
318
+
319
+
320
+ class UnsupportedAuthSchemeError(ASAPError):
321
+ """Raised when an unsupported authentication scheme is specified.
322
+
323
+ This error occurs when a Manifest specifies an authentication scheme
324
+ that is not supported by the current implementation.
325
+
326
+ Attributes:
327
+ scheme: The unsupported scheme name
328
+ supported_schemes: List of supported schemes
329
+ """
330
+
331
+ def __init__(
332
+ self,
333
+ scheme: str,
334
+ supported_schemes: set[str] | frozenset[str],
335
+ details: dict[str, Any] | None = None,
336
+ ) -> None:
337
+ """Initialize unsupported auth scheme error.
338
+
339
+ Args:
340
+ scheme: The unsupported scheme name
341
+ supported_schemes: Set of supported schemes
342
+ details: Optional additional context
343
+ """
344
+ supported_list = sorted(supported_schemes)
345
+ message = (
346
+ f"Unsupported authentication scheme '{scheme}'. "
347
+ f"Supported schemes: {', '.join(supported_list)}"
348
+ )
349
+ super().__init__(
350
+ code="asap:auth/unsupported_scheme",
351
+ message=message,
352
+ details={
353
+ "scheme": scheme,
354
+ "supported_schemes": list(supported_list),
355
+ **(details or {}),
356
+ },
357
+ )
358
+ self.scheme = scheme
359
+ self.supported_schemes = supported_schemes
asap/examples/README.md CHANGED
@@ -1,25 +1,96 @@
1
+ # ASAP Protocol Examples
2
+
3
+ This directory contains real-world examples for the ASAP protocol: minimal agents, demos, and patterns you can reuse.
4
+
1
5
  ## Overview
2
6
 
3
- The examples demonstrate a minimal end-to-end flow between two agents:
4
- an echo agent and a coordinator agent.
7
+ Examples cover:
8
+
9
+ - **Core flow**: Echo agent, coordinator, and a full demo (run_demo).
10
+ - **Advanced patterns**: Multi-agent orchestration, long-running tasks with checkpoints, error recovery, MCP integration, state migration, auth, rate limiting.
11
+ - **Concepts**: WebSocket (not implemented), streaming responses, multi-step workflows.
5
12
 
6
- ## Running the demo
13
+ Run any example from the repository root with:
7
14
 
8
- Run the demo runner module from the repository root:
15
+ ```bash
16
+ uv run python -m asap.examples.<module_name> [options]
17
+ ```
9
18
 
10
- - `uv run python -m asap.examples.run_demo`
19
+ ## Running the full demo
11
20
 
12
- This starts the echo agent on port 8001 and the coordinator agent on port 8000.
13
- The coordinator sends a TaskRequest to the echo agent and logs the response.
21
+ Starts the echo agent on port 8001 and the coordinator on port 8000; the coordinator sends a TaskRequest to the echo agent and logs the response.
14
22
 
15
- ## Running agents individually
23
+ ```bash
24
+ uv run python -m asap.examples.run_demo
25
+ ```
16
26
 
17
- You can run the agents separately if needed:
27
+ Run agents individually:
18
28
 
19
29
  - `uv run python -m asap.examples.echo_agent --host 127.0.0.1 --port 8001`
20
- - `uv run python -m asap.examples.coordinator`
30
+ - `uv run python -m asap.examples.coordinator --echo-url http://127.0.0.1:8001`
31
+
32
+ ---
33
+
34
+ ## Examples by topic
35
+
36
+ ### Core agents and demo
37
+
38
+ | Module | Description | Usage |
39
+ |--------|-------------|--------|
40
+ | **run_demo** | Full demo: echo + coordinator, one TaskRequest round-trip | `uv run python -m asap.examples.run_demo` |
41
+ | **echo_agent** | Minimal echo agent (FastAPI app, manifest, echo handler) | `uv run python -m asap.examples.echo_agent [--host H] [--port P]` |
42
+ | **coordinator** | Coordinator that dispatches TaskRequest to echo agent | `uv run python -m asap.examples.coordinator [--echo-url URL] [--message MSG]` |
43
+ | **secure_handler** | Reference handler: TaskRequest validation, FilePart URI checks, sanitize_for_logging | Use `create_secure_handler()` in your handler registry (see `docs/security.md`) |
44
+
45
+ ### Multi-agent and orchestration
46
+
47
+ | Module | Description | Usage |
48
+ |--------|-------------|--------|
49
+ | **orchestration** | Main agent delegates to 2 sub-agents; task coordination and state tracking | `uv run python -m asap.examples.orchestration [--worker-a-url URL] [--worker-b-url URL]` (start two echo agents on 8001 and 8002 first) |
50
+
51
+ ### State and long-running tasks
52
+
53
+ | Module | Description | Usage |
54
+ |--------|-------------|--------|
55
+ | **long_running** | Long-running task with checkpoints (StateSnapshot); save, “crash”, resume | `uv run python -m asap.examples.long_running [--num-steps N] [--crash-after N]` |
56
+ | **state_migration** | Move task state between agents (StateQuery, StateRestore, SnapshotStore) | `uv run python -m asap.examples.state_migration` |
57
+
58
+ ### Error recovery and resilience
59
+
60
+ | Module | Description | Usage |
61
+ |--------|-------------|--------|
62
+ | **error_recovery** | Retry with backoff, circuit breaker, fallback patterns | `uv run python -m asap.examples.error_recovery [--skip-retry] [--skip-circuit] [--skip-fallback]` |
63
+
64
+ ### MCP and integration
65
+
66
+ | Module | Description | Usage |
67
+ |--------|-------------|--------|
68
+ | **mcp_integration** | Call MCP tools via ASAP envelopes (McpToolCall, McpToolResult) | `uv run python -m asap.examples.mcp_integration [--agent-url URL]` (local build only if no URL) |
69
+
70
+ ### Authentication and rate limiting
71
+
72
+ | Module | Description | Usage |
73
+ |--------|-------------|--------|
74
+ | **auth_patterns** | Bearer auth, custom token validators, OAuth2 concept (manifest + create_app) | `uv run python -m asap.examples.auth_patterns` |
75
+ | **rate_limiting** | Per-sender and per-endpoint rate limit patterns (create_limiter, ASAP_RATE_LIMIT) | `uv run python -m asap.examples.rate_limiting` |
76
+
77
+ ### Concepts (no full implementation)
78
+
79
+ | Module | Description | Usage |
80
+ |--------|-------------|--------|
81
+ | **websocket_concept** | How WebSocket would work with ASAP (comments/pseudocode only) | `uv run python -m asap.examples.websocket_concept` |
82
+
83
+ ### Streaming and workflows
84
+
85
+ | Module | Description | Usage |
86
+ |--------|-------------|--------|
87
+ | **streaming_response** | Stream TaskUpdate progress chunks (simulated streaming) | `uv run python -m asap.examples.streaming_response [--chunks N]` |
88
+ | **multi_step_workflow** | Multi-step pipeline: fetch → transform → summarize (WorkflowState, run_workflow) | `uv run python -m asap.examples.multi_step_workflow` |
89
+
90
+ ---
21
91
 
22
92
  ## Notes
23
93
 
24
94
  - The echo agent exposes `/.well-known/asap/manifest.json` for readiness checks.
25
95
  - Update ports in `asap.examples.run_demo` if you change the defaults.
96
+ - Examples use the basic ASAP API; for production, add authentication via `manifest.auth` and follow `docs/security.md`.
@@ -0,0 +1,212 @@
1
+ """Authentication patterns example for ASAP protocol.
2
+
3
+ This module shows how to configure Bearer token auth, custom token validators,
4
+ and the OAuth2 concept (obtain Bearer tokens via OAuth2; ASAP validates Bearer).
5
+
6
+ Patterns:
7
+ 1. Bearer: AuthScheme(schemes=["bearer"]) and token_validator for create_app.
8
+ 2. Custom validators: Static map, env-based, or callable(token) -> agent_id | None.
9
+ 3. OAuth2 concept: oauth2 dict in AuthScheme for discovery; clients get Bearer via OAuth2.
10
+
11
+ Run:
12
+ uv run python -m asap.examples.auth_patterns
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import argparse
18
+ import os
19
+ from typing import Callable, Sequence
20
+
21
+ from asap.models.entities import AuthScheme, Capability, Endpoint, Manifest, Skill
22
+ from asap.models.constants import SUPPORTED_AUTH_SCHEMES
23
+ from asap.observability import get_logger
24
+ from asap.transport.middleware import BearerTokenValidator
25
+ from asap.transport.server import create_app
26
+
27
+ logger = get_logger(__name__)
28
+
29
+ DEFAULT_ASAP_ENDPOINT = "http://localhost:8000/asap"
30
+ DEFAULT_AGENT_ID = "urn:asap:agent:secured"
31
+
32
+
33
+ def build_manifest_bearer_only(asap_endpoint: str = DEFAULT_ASAP_ENDPOINT) -> Manifest:
34
+ """Build a manifest that requires Bearer token authentication.
35
+
36
+ Use with create_app(manifest, token_validator=your_validator).
37
+ Only schemes in SUPPORTED_AUTH_SCHEMES are allowed (e.g. bearer, basic).
38
+
39
+ Args:
40
+ asap_endpoint: URL where the agent receives ASAP messages.
41
+
42
+ Returns:
43
+ Manifest with auth=AuthScheme(schemes=["bearer"]).
44
+ """
45
+ return Manifest(
46
+ id=DEFAULT_AGENT_ID,
47
+ name="Secured Agent",
48
+ version="0.1.0",
49
+ description="Agent with Bearer token authentication",
50
+ capabilities=Capability(
51
+ asap_version="0.1",
52
+ skills=[Skill(id="execute", description="Execute tasks")],
53
+ state_persistence=False,
54
+ ),
55
+ endpoints=Endpoint(asap=asap_endpoint),
56
+ auth=AuthScheme(schemes=["bearer"]),
57
+ )
58
+
59
+
60
+ def build_manifest_oauth2_concept(asap_endpoint: str = DEFAULT_ASAP_ENDPOINT) -> Manifest:
61
+ """Build a manifest with Bearer + OAuth2 discovery (concept).
62
+
63
+ ASAP currently validates Bearer tokens only. The oauth2 dict describes
64
+ where clients obtain tokens (authorization_url, token_url, scopes).
65
+ Clients perform OAuth2 flow externally and send the access_token as Bearer.
66
+
67
+ Args:
68
+ asap_endpoint: URL where the agent receives ASAP messages.
69
+
70
+ Returns:
71
+ Manifest with auth=AuthScheme(schemes=["bearer"], oauth2={...}).
72
+ """
73
+ return Manifest(
74
+ id=DEFAULT_AGENT_ID,
75
+ name="OAuth2-Aware Agent",
76
+ version="0.1.0",
77
+ description="Agent with Bearer auth; clients get tokens via OAuth2",
78
+ capabilities=Capability(
79
+ asap_version="0.1",
80
+ skills=[Skill(id="execute", description="Execute tasks")],
81
+ state_persistence=False,
82
+ ),
83
+ endpoints=Endpoint(asap=asap_endpoint),
84
+ auth=AuthScheme(
85
+ schemes=["bearer"],
86
+ oauth2={
87
+ "authorization_url": "https://auth.example.com/authorize", # nosec B105
88
+ "token_url": "https://auth.example.com/token", # nosec B105
89
+ "scopes": ["asap:execute", "asap:read"],
90
+ },
91
+ ),
92
+ )
93
+
94
+
95
+ def static_map_validator(token_to_agent: dict[str, str]) -> Callable[[str], str | None]:
96
+ """Build a token validator that maps known tokens to agent IDs.
97
+
98
+ Use for demos or small fixed sets. For production, use a secure store or JWT.
99
+
100
+ Args:
101
+ token_to_agent: Map from token string to agent URN.
102
+
103
+ Returns:
104
+ Callable(token) -> agent_id or None.
105
+ """
106
+
107
+ def validate(token: str) -> str | None:
108
+ return token_to_agent.get(token)
109
+
110
+ return validate
111
+
112
+
113
+ def env_based_validator(
114
+ env_var: str = "ASAP_DEMO_TOKEN",
115
+ expected_agent_id: str = "urn:asap:agent:demo-client",
116
+ ) -> Callable[[str], str | None]:
117
+ """Build a token validator that accepts a token from an environment variable.
118
+
119
+ Use for testing; avoid in production (env vars can be inspected).
120
+
121
+ Args:
122
+ env_var: Environment variable holding the valid token.
123
+ expected_agent_id: Agent ID to return when token matches.
124
+
125
+ Returns:
126
+ Callable(token) -> agent_id or None.
127
+ """
128
+ expected_token = os.environ.get(env_var, "")
129
+
130
+ def validate(token: str) -> str | None:
131
+ if token and token == expected_token:
132
+ return expected_agent_id
133
+ return None
134
+
135
+ return validate
136
+
137
+
138
+ def run_demo() -> None:
139
+ """Demonstrate auth patterns: Bearer manifest, custom validators, OAuth2 concept."""
140
+ # Bearer-only manifest
141
+ manifest_bearer = build_manifest_bearer_only()
142
+ logger.info(
143
+ "asap.auth_patterns.bearer_manifest",
144
+ schemes=manifest_bearer.auth.schemes if manifest_bearer.auth else [],
145
+ )
146
+
147
+ # Custom validator: static map
148
+ token_map = {
149
+ "demo-token-123": "urn:asap:agent:client-a",
150
+ "other-token": "urn:asap:agent:client-b",
151
+ }
152
+ validator_static = static_map_validator(token_map)
153
+ bearer_validator = BearerTokenValidator(validator_static)
154
+ agent_id = bearer_validator("demo-token-123")
155
+ logger.info(
156
+ "asap.auth_patterns.static_validator",
157
+ token_preview="demo-token-***", # nosec B106
158
+ agent_id=agent_id,
159
+ )
160
+ assert agent_id == "urn:asap:agent:client-a" # nosec B101
161
+ assert bearer_validator("invalid") is None # nosec B101
162
+
163
+ # Custom validator: env-based (no env set -> invalid)
164
+ validator_env = env_based_validator(env_var="ASAP_DEMO_TOKEN")
165
+ assert validator_env("any") is None # nosec B101
166
+ logger.info(
167
+ "asap.auth_patterns.env_validator",
168
+ message="Use ASAP_DEMO_TOKEN to test env-based validator",
169
+ )
170
+
171
+ # OAuth2 concept manifest (Bearer + oauth2 discovery)
172
+ manifest_oauth2 = build_manifest_oauth2_concept()
173
+ logger.info(
174
+ "asap.auth_patterns.oauth2_concept",
175
+ schemes=manifest_oauth2.auth.schemes if manifest_oauth2.auth else [],
176
+ oauth2_urls=(
177
+ list(manifest_oauth2.auth.oauth2.keys())
178
+ if manifest_oauth2.auth and manifest_oauth2.auth.oauth2
179
+ else []
180
+ ),
181
+ )
182
+
183
+ # Wire app with Bearer auth (no server started; just create_app)
184
+ app = create_app(
185
+ manifest_bearer,
186
+ token_validator=validator_static,
187
+ )
188
+ logger.info(
189
+ "asap.auth_patterns.app_created",
190
+ message="create_app(manifest, token_validator=...) enables Bearer auth",
191
+ supported_schemes=list(SUPPORTED_AUTH_SCHEMES),
192
+ )
193
+ # Reference app so it's not garbage-collected if someone holds the result
194
+ assert app is not None # nosec B101
195
+
196
+
197
+ def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace:
198
+ """Parse command-line arguments for the auth patterns demo."""
199
+ parser = argparse.ArgumentParser(
200
+ description="Authentication patterns: Bearer, custom validators, OAuth2 concept."
201
+ )
202
+ return parser.parse_args(argv)
203
+
204
+
205
+ def main(argv: Sequence[str] | None = None) -> None:
206
+ """Run the auth patterns demo."""
207
+ parse_args(argv)
208
+ run_demo()
209
+
210
+
211
+ if __name__ == "__main__":
212
+ main()