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/__init__.py
CHANGED
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,
|
|
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/examples/README.md
CHANGED
|
@@ -1,28 +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
|
-
|
|
4
|
-
|
|
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
|
-
|
|
13
|
+
Run any example from the repository root with:
|
|
7
14
|
|
|
8
|
-
|
|
15
|
+
```bash
|
|
16
|
+
uv run python -m asap.examples.<module_name> [options]
|
|
17
|
+
```
|
|
9
18
|
|
|
10
|
-
|
|
19
|
+
## Running the full demo
|
|
11
20
|
|
|
12
|
-
|
|
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
|
-
|
|
23
|
+
```bash
|
|
24
|
+
uv run python -m asap.examples.run_demo
|
|
25
|
+
```
|
|
16
26
|
|
|
17
|
-
|
|
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.
|
|
26
|
-
-
|
|
27
|
-
For production use, consider adding authentication via `manifest.auth` and enabling
|
|
28
|
-
additional security features (see `docs/security.md`).
|
|
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()
|