a2a-handler 0.1.21__tar.gz → 0.1.23__tar.gz
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.
- {a2a_handler-0.1.21 → a2a_handler-0.1.23}/PKG-INFO +42 -37
- a2a_handler-0.1.23/README.md +81 -0
- {a2a_handler-0.1.21 → a2a_handler-0.1.23}/pyproject.toml +3 -3
- {a2a_handler-0.1.21 → a2a_handler-0.1.23}/src/a2a_handler/auth.py +4 -0
- {a2a_handler-0.1.21 → a2a_handler-0.1.23}/src/a2a_handler/cli/__init__.py +139 -5
- {a2a_handler-0.1.21 → a2a_handler-0.1.23}/src/a2a_handler/cli/_helpers.py +102 -3
- {a2a_handler-0.1.21 → a2a_handler-0.1.23}/src/a2a_handler/cli/card.py +9 -2
- {a2a_handler-0.1.21 → a2a_handler-0.1.23}/src/a2a_handler/cli/message.py +285 -14
- {a2a_handler-0.1.21 → a2a_handler-0.1.23}/src/a2a_handler/cli/schema.py +3 -4
- {a2a_handler-0.1.21 → a2a_handler-0.1.23}/src/a2a_handler/cli/server.py +2 -2
- {a2a_handler-0.1.21 → a2a_handler-0.1.23}/src/a2a_handler/cli/session.py +8 -5
- {a2a_handler-0.1.21 → a2a_handler-0.1.23}/src/a2a_handler/cli/task.py +4 -1
- {a2a_handler-0.1.21 → a2a_handler-0.1.23}/src/a2a_handler/common/logging.py +35 -13
- a2a_handler-0.1.23/src/a2a_handler/common/output.py +113 -0
- {a2a_handler-0.1.21 → a2a_handler-0.1.23}/src/a2a_handler/server/__init__.py +1 -1
- a2a_handler-0.1.23/src/a2a_handler/server/agent.py +133 -0
- {a2a_handler-0.1.21 → a2a_handler-0.1.23}/src/a2a_handler/server/card.py +58 -3
- a2a_handler-0.1.23/src/a2a_handler/server/tools/__init__.py +23 -0
- a2a_handler-0.1.23/src/a2a_handler/server/tools/a2a_docs.py +140 -0
- a2a_handler-0.1.23/src/a2a_handler/server/tools/handler_docs.py +44 -0
- a2a_handler-0.1.23/src/a2a_handler/server/tools/source.py +109 -0
- {a2a_handler-0.1.21 → a2a_handler-0.1.23}/src/a2a_handler/servers.py +35 -0
- {a2a_handler-0.1.21 → a2a_handler-0.1.23}/src/a2a_handler/service.py +3 -1
- {a2a_handler-0.1.21 → a2a_handler-0.1.23}/src/a2a_handler/tui/app.py +48 -10
- {a2a_handler-0.1.21 → a2a_handler-0.1.23}/src/a2a_handler/tui/app.tcss +154 -12
- {a2a_handler-0.1.21 → a2a_handler-0.1.23}/src/a2a_handler/tui/components/artifacts.py +6 -19
- {a2a_handler-0.1.21 → a2a_handler-0.1.23}/src/a2a_handler/tui/components/card.py +6 -4
- {a2a_handler-0.1.21 → a2a_handler-0.1.23}/src/a2a_handler/tui/components/input.py +29 -1
- {a2a_handler-0.1.21 → a2a_handler-0.1.23}/src/a2a_handler/tui/components/messages.py +188 -19
- {a2a_handler-0.1.21 → a2a_handler-0.1.23}/src/a2a_handler/tui/components/tasks.py +99 -32
- {a2a_handler-0.1.21 → a2a_handler-0.1.23}/src/a2a_handler/tui/server/tab.py +199 -0
- {a2a_handler-0.1.21 → a2a_handler-0.1.23}/src/a2a_handler/tui/server/views.py +13 -4
- a2a_handler-0.1.21/README.md +0 -76
- a2a_handler-0.1.21/src/a2a_handler/common/output.py +0 -66
- a2a_handler-0.1.21/src/a2a_handler/server/agent.py +0 -76
- {a2a_handler-0.1.21 → a2a_handler-0.1.23}/LICENSE +0 -0
- {a2a_handler-0.1.21 → a2a_handler-0.1.23}/src/a2a_handler/__init__.py +0 -0
- {a2a_handler-0.1.21 → a2a_handler-0.1.23}/src/a2a_handler/cli/mcp.py +0 -0
- {a2a_handler-0.1.21 → a2a_handler-0.1.23}/src/a2a_handler/common/__init__.py +0 -0
- {a2a_handler-0.1.21 → a2a_handler-0.1.23}/src/a2a_handler/common/config.py +0 -0
- {a2a_handler-0.1.21 → a2a_handler-0.1.23}/src/a2a_handler/common/dotenv.py +0 -0
- {a2a_handler-0.1.21 → a2a_handler-0.1.23}/src/a2a_handler/common/input_validation.py +0 -0
- {a2a_handler-0.1.21 → a2a_handler-0.1.23}/src/a2a_handler/mcp/__init__.py +0 -0
- {a2a_handler-0.1.21 → a2a_handler-0.1.23}/src/a2a_handler/mcp/server.py +0 -0
- {a2a_handler-0.1.21 → a2a_handler-0.1.23}/src/a2a_handler/server/app.py +0 -0
- {a2a_handler-0.1.21 → a2a_handler-0.1.23}/src/a2a_handler/server/ollama.py +0 -0
- {a2a_handler-0.1.21 → a2a_handler-0.1.23}/src/a2a_handler/session.py +0 -0
- {a2a_handler-0.1.21 → a2a_handler-0.1.23}/src/a2a_handler/tui/__init__.py +0 -0
- {a2a_handler-0.1.21 → a2a_handler-0.1.23}/src/a2a_handler/tui/commands/__init__.py +0 -0
- {a2a_handler-0.1.21 → a2a_handler-0.1.23}/src/a2a_handler/tui/commands/palette.py +0 -0
- {a2a_handler-0.1.21 → a2a_handler-0.1.23}/src/a2a_handler/tui/commands/screens.py +0 -0
- {a2a_handler-0.1.21 → a2a_handler-0.1.23}/src/a2a_handler/tui/components/__init__.py +0 -0
- {a2a_handler-0.1.21 → a2a_handler-0.1.23}/src/a2a_handler/tui/components/auth.py +0 -0
- {a2a_handler-0.1.21 → a2a_handler-0.1.23}/src/a2a_handler/tui/components/headers.py +0 -0
- {a2a_handler-0.1.21 → a2a_handler-0.1.23}/src/a2a_handler/tui/components/logs.py +0 -0
- {a2a_handler-0.1.21 → a2a_handler-0.1.23}/src/a2a_handler/tui/server/__init__.py +0 -0
- {a2a_handler-0.1.21 → a2a_handler-0.1.23}/src/a2a_handler/tui/server/save.py +0 -0
- {a2a_handler-0.1.21 → a2a_handler-0.1.23}/src/a2a_handler/tui/server/session.py +0 -0
- {a2a_handler-0.1.21 → a2a_handler-0.1.23}/src/a2a_handler/tui/server/tabs.py +0 -0
- {a2a_handler-0.1.21 → a2a_handler-0.1.23}/src/a2a_handler/tui/server/types.py +0 -0
- {a2a_handler-0.1.21 → a2a_handler-0.1.23}/src/a2a_handler/tui/server/workspace.py +0 -0
- {a2a_handler-0.1.21 → a2a_handler-0.1.23}/src/a2a_handler/validation.py +0 -0
- {a2a_handler-0.1.21 → a2a_handler-0.1.23}/src/a2a_handler/webhook.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: a2a-handler
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.23
|
|
4
4
|
Summary: An A2A Protocol client TUI and CLI.
|
|
5
5
|
Keywords: a2a,a2a-protocol,agent,agent-to-agent,ai,cli,tui
|
|
6
6
|
Author: Al Duncanson
|
|
@@ -20,7 +20,7 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
|
20
20
|
Classifier: Typing :: Typed
|
|
21
21
|
Requires-Dist: a2a-sdk>=0.3.24,<1.0.0
|
|
22
22
|
Requires-Dist: click>=8.0.0
|
|
23
|
-
Requires-Dist: google-adk>=
|
|
23
|
+
Requires-Dist: google-adk>=2.0.0
|
|
24
24
|
Requires-Dist: httpx>=0.27.0
|
|
25
25
|
Requires-Dist: litellm>=1.0.0
|
|
26
26
|
Requires-Dist: python-dotenv>=1.0.0
|
|
@@ -28,7 +28,7 @@ Requires-Dist: rich>=13.0.0
|
|
|
28
28
|
Requires-Dist: textual>=0.47.0
|
|
29
29
|
Requires-Dist: uvicorn>=0.30.0
|
|
30
30
|
Requires-Dist: textual-serve>=1.1.0
|
|
31
|
-
Requires-Dist: mcp[cli]>=1.
|
|
31
|
+
Requires-Dist: mcp[cli]>=1.27.1
|
|
32
32
|
Requires-Dist: truststore>=0.9.0
|
|
33
33
|
Requires-Dist: platformdirs>=4.0.0
|
|
34
34
|
Requires-Python: >=3.11
|
|
@@ -45,73 +45,78 @@ Description-Content-Type: text/markdown
|
|
|
45
45
|
[](https://a2a-protocol.org/latest/)
|
|
46
46
|
[](https://pypi.org/project/a2a-handler/)
|
|
47
47
|
[](https://pypi.org/project/a2a-handler/)
|
|
48
|
-
[](https://pypi.org/project/a2a-handler/)
|
|
49
48
|
[](https://pepy.tech/projects/a2a-handler)
|
|
50
49
|
[](https://github.com/alDuncanson/handler/stargazers)
|
|
51
50
|
|
|
52
|
-
|
|
51
|
+
Handler is an open-source [A2A protocol](https://github.com/a2aproject/A2A)
|
|
52
|
+
client for software engineers building, testing, and operating agentic systems.
|
|
53
|
+
It provides an interactive TUI, a scriptable CLI with structured output, and an
|
|
54
|
+
MCP server that lets other agents integrate with A2A services directly. Handler
|
|
55
|
+
also supports global and repo-scoped A2A server configuration with bearer,
|
|
56
|
+
API key, mTLS, and OAuth2 client credentials auth.
|
|
53
57
|
|
|
54
|
-
Handler
|
|
55
|
-
|
|
56
|
-
Use the TUI or CLI to send messages, manage tasks, and inspect agent cards.
|
|
57
|
-
An included MCP server lets your AI assistant talk to A2A agents too.
|
|
58
|
+

|
|
58
59
|
|
|
59
60
|
## Install
|
|
60
61
|
|
|
62
|
+
Install Handler from the [PyPI package](https://pypi.org/project/a2a-handler/) as a `uv` tool:
|
|
63
|
+
|
|
61
64
|
```bash
|
|
62
65
|
uv tool install a2a-handler
|
|
63
66
|
```
|
|
64
67
|
|
|
65
|
-
Or with [pipx](https://pipx.pypa.io/)
|
|
68
|
+
Or with [pipx](https://pipx.pypa.io/):
|
|
66
69
|
|
|
67
70
|
```bash
|
|
68
|
-
pipx install a2a-handler
|
|
71
|
+
pipx install a2a-handler
|
|
69
72
|
```
|
|
70
73
|
|
|
71
|
-
|
|
74
|
+
Or with pip:
|
|
72
75
|
|
|
73
76
|
```bash
|
|
74
|
-
|
|
75
|
-
handler message send URL "hello" # send a message from the CLI
|
|
76
|
-
handler card get URL # fetch an agent card
|
|
77
|
-
handler mcp # start the MCP server
|
|
78
|
-
handler server run agent # run the bundled reference agent
|
|
77
|
+
pip install a2a-handler
|
|
79
78
|
```
|
|
80
79
|
|
|
81
|
-
##
|
|
80
|
+
## Quick Start
|
|
81
|
+
|
|
82
|
+
Open the interactive terminal UI:
|
|
82
83
|
|
|
83
84
|
```bash
|
|
84
|
-
|
|
85
|
+
handler tui
|
|
85
86
|
```
|
|
86
87
|
|
|
87
|
-
|
|
88
|
+
Inspect an A2A server's agent card:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
handler card get --url http://localhost:8000
|
|
92
|
+
```
|
|
88
93
|
|
|
89
|
-
|
|
90
|
-
(global) and `.handler/servers.toml` (repo-local).
|
|
94
|
+
Send a message from the CLI:
|
|
91
95
|
|
|
92
|
-
```
|
|
93
|
-
|
|
96
|
+
```bash
|
|
97
|
+
handler message send --url URL --text "hello"
|
|
98
|
+
```
|
|
94
99
|
|
|
95
|
-
|
|
96
|
-
url = "http://localhost:8000"
|
|
100
|
+
Open the full documentation:
|
|
97
101
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
env = "HANDLER_LOCAL_TOKEN"
|
|
102
|
+
```bash
|
|
103
|
+
handler docs
|
|
101
104
|
```
|
|
102
105
|
|
|
103
|
-
|
|
104
|
-
|
|
106
|
+
## Run Without Installing
|
|
107
|
+
|
|
108
|
+
Run Handler with `uvx`:
|
|
105
109
|
|
|
106
|
-
|
|
110
|
+
```bash
|
|
111
|
+
uvx --from a2a-handler handler
|
|
112
|
+
```
|
|
107
113
|
|
|
108
|
-
|
|
109
|
-
is available via [Nix](https://zero-to-nix.com/concepts/nix/):
|
|
114
|
+
Run Handler with `pipx`:
|
|
110
115
|
|
|
111
116
|
```bash
|
|
112
|
-
|
|
117
|
+
pipx run a2a-handler
|
|
113
118
|
```
|
|
114
119
|
|
|
115
|
-
|
|
120
|
+
## Documentation
|
|
116
121
|
|
|
117
|
-
|
|
122
|
+
Read the documentation at <https://handler.alduncanson.com>.
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Handler
|
|
2
|
+
|
|
3
|
+
[](https://github.com/alDuncanson/handler/actions/workflows/ci.yml)
|
|
4
|
+
[](https://a2a-protocol.org/latest/)
|
|
5
|
+
[](https://pypi.org/project/a2a-handler/)
|
|
6
|
+
[](https://pypi.org/project/a2a-handler/)
|
|
7
|
+
[](https://pepy.tech/projects/a2a-handler)
|
|
8
|
+
[](https://github.com/alDuncanson/handler/stargazers)
|
|
9
|
+
|
|
10
|
+
Handler is an open-source [A2A protocol](https://github.com/a2aproject/A2A)
|
|
11
|
+
client for software engineers building, testing, and operating agentic systems.
|
|
12
|
+
It provides an interactive TUI, a scriptable CLI with structured output, and an
|
|
13
|
+
MCP server that lets other agents integrate with A2A services directly. Handler
|
|
14
|
+
also supports global and repo-scoped A2A server configuration with bearer,
|
|
15
|
+
API key, mTLS, and OAuth2 client credentials auth.
|
|
16
|
+
|
|
17
|
+

|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
Install Handler from the [PyPI package](https://pypi.org/project/a2a-handler/) as a `uv` tool:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
uv tool install a2a-handler
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Or with [pipx](https://pipx.pypa.io/):
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pipx install a2a-handler
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Or with pip:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install a2a-handler
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Quick Start
|
|
40
|
+
|
|
41
|
+
Open the interactive terminal UI:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
handler tui
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Inspect an A2A server's agent card:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
handler card get --url http://localhost:8000
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Send a message from the CLI:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
handler message send --url URL --text "hello"
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Open the full documentation:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
handler docs
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Run Without Installing
|
|
66
|
+
|
|
67
|
+
Run Handler with `uvx`:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
uvx --from a2a-handler handler
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Run Handler with `pipx`:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
pipx run a2a-handler
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Documentation
|
|
80
|
+
|
|
81
|
+
Read the documentation at <https://handler.alduncanson.com>.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "a2a-handler"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.23"
|
|
4
4
|
description = "An A2A Protocol client TUI and CLI."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.11"
|
|
@@ -34,7 +34,7 @@ classifiers = [
|
|
|
34
34
|
dependencies = [
|
|
35
35
|
"a2a-sdk>=0.3.24,<1.0.0",
|
|
36
36
|
"click>=8.0.0",
|
|
37
|
-
"google-adk>=
|
|
37
|
+
"google-adk>=2.0.0",
|
|
38
38
|
"httpx>=0.27.0",
|
|
39
39
|
"litellm>=1.0.0",
|
|
40
40
|
"python-dotenv>=1.0.0",
|
|
@@ -42,7 +42,7 @@ dependencies = [
|
|
|
42
42
|
"textual>=0.47.0",
|
|
43
43
|
"uvicorn>=0.30.0",
|
|
44
44
|
"textual-serve>=1.1.0",
|
|
45
|
-
"mcp[cli]>=1.
|
|
45
|
+
"mcp[cli]>=1.27.1",
|
|
46
46
|
"truststore>=0.9.0",
|
|
47
47
|
"platformdirs>=4.0.0",
|
|
48
48
|
]
|
|
@@ -250,6 +250,10 @@ def create_mtls_auth(
|
|
|
250
250
|
ca_cert_path: str | None = None,
|
|
251
251
|
) -> AuthCredentials:
|
|
252
252
|
"""Create mTLS (mutual TLS) client certificate authentication."""
|
|
253
|
+
cert_path = str(Path(cert_path).expanduser())
|
|
254
|
+
key_path = str(Path(key_path).expanduser())
|
|
255
|
+
ca_cert_path = str(Path(ca_cert_path).expanduser()) if ca_cert_path else None
|
|
256
|
+
|
|
253
257
|
if not Path(cert_path).is_file():
|
|
254
258
|
raise FileNotFoundError(f"Client certificate not found: {cert_path}")
|
|
255
259
|
if not Path(key_path).is_file():
|
|
@@ -19,6 +19,11 @@ truststore.inject_into_ssl()
|
|
|
19
19
|
|
|
20
20
|
import logging
|
|
21
21
|
import os
|
|
22
|
+
import shutil
|
|
23
|
+
import subprocess
|
|
24
|
+
import webbrowser
|
|
25
|
+
from collections.abc import Sequence
|
|
26
|
+
from typing import Any
|
|
22
27
|
|
|
23
28
|
logging.getLogger().setLevel(logging.WARNING)
|
|
24
29
|
|
|
@@ -30,6 +35,7 @@ from a2a_handler.common.dotenv import load_runtime_dotenv
|
|
|
30
35
|
from a2a_handler.common.output import OutputFormat
|
|
31
36
|
from a2a_handler.tui import HandlerTUI
|
|
32
37
|
|
|
38
|
+
from ._helpers import configure_http_timeouts
|
|
33
39
|
from .card import card
|
|
34
40
|
from .mcp import mcp
|
|
35
41
|
from .message import message
|
|
@@ -40,24 +46,81 @@ from .task import task
|
|
|
40
46
|
|
|
41
47
|
log = get_logger(__name__)
|
|
42
48
|
|
|
49
|
+
DOCS_URL = "https://handler.alduncanson.com/"
|
|
50
|
+
PACKAGE_NAME = "a2a-handler"
|
|
43
51
|
|
|
44
|
-
|
|
52
|
+
|
|
53
|
+
class RuntimeDotenvGroup(click.Group):
|
|
54
|
+
"""Click group that loads workspace .env before option envvars resolve."""
|
|
55
|
+
|
|
56
|
+
def main(
|
|
57
|
+
self,
|
|
58
|
+
args: Sequence[str] | None = None,
|
|
59
|
+
prog_name: str | None = None,
|
|
60
|
+
complete_var: str | None = None,
|
|
61
|
+
standalone_mode: bool = True,
|
|
62
|
+
windows_expand_args: bool = True,
|
|
63
|
+
**extra: Any,
|
|
64
|
+
) -> Any:
|
|
65
|
+
"""Load runtime dotenv before Click parses envvar-backed options."""
|
|
66
|
+
load_runtime_dotenv()
|
|
67
|
+
return super().main(
|
|
68
|
+
args=args,
|
|
69
|
+
prog_name=prog_name,
|
|
70
|
+
complete_var=complete_var,
|
|
71
|
+
standalone_mode=standalone_mode,
|
|
72
|
+
windows_expand_args=windows_expand_args,
|
|
73
|
+
**extra,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@click.group(cls=RuntimeDotenvGroup)
|
|
45
78
|
@click.version_option(version=__version__, prog_name="handler")
|
|
46
79
|
@click.option("--verbose", "-v", is_flag=True, help="Enable verbose logging")
|
|
47
80
|
@click.option("--debug", "-d", is_flag=True, help="Enable debug logging")
|
|
48
81
|
@click.option(
|
|
49
82
|
"--output",
|
|
50
83
|
"output_format",
|
|
51
|
-
type=click.Choice(["json", "ndjson"]),
|
|
52
|
-
default="
|
|
84
|
+
type=click.Choice(["text", "json", "ndjson"]),
|
|
85
|
+
default="text",
|
|
53
86
|
show_default=True,
|
|
54
|
-
help="Output format",
|
|
87
|
+
help="Output format (use json/ndjson for structured output)",
|
|
55
88
|
)
|
|
56
89
|
@click.option(
|
|
57
90
|
"--quiet",
|
|
58
91
|
is_flag=True,
|
|
59
92
|
help="Suppress non-error output",
|
|
60
93
|
)
|
|
94
|
+
@click.option(
|
|
95
|
+
"--connect-timeout",
|
|
96
|
+
envvar="HANDLER_CONNECT_TIMEOUT",
|
|
97
|
+
metavar="SECONDS|none",
|
|
98
|
+
help="HTTP connect timeout in seconds (env: HANDLER_CONNECT_TIMEOUT)",
|
|
99
|
+
)
|
|
100
|
+
@click.option(
|
|
101
|
+
"--read-timeout",
|
|
102
|
+
envvar="HANDLER_READ_TIMEOUT",
|
|
103
|
+
metavar="SECONDS|none",
|
|
104
|
+
help="HTTP read timeout for non-streaming calls (env: HANDLER_READ_TIMEOUT)",
|
|
105
|
+
)
|
|
106
|
+
@click.option(
|
|
107
|
+
"--write-timeout",
|
|
108
|
+
envvar="HANDLER_WRITE_TIMEOUT",
|
|
109
|
+
metavar="SECONDS|none",
|
|
110
|
+
help="HTTP write timeout in seconds (env: HANDLER_WRITE_TIMEOUT)",
|
|
111
|
+
)
|
|
112
|
+
@click.option(
|
|
113
|
+
"--pool-timeout",
|
|
114
|
+
envvar="HANDLER_POOL_TIMEOUT",
|
|
115
|
+
metavar="SECONDS|none",
|
|
116
|
+
help="HTTP connection pool timeout in seconds (env: HANDLER_POOL_TIMEOUT)",
|
|
117
|
+
)
|
|
118
|
+
@click.option(
|
|
119
|
+
"--stream-read-timeout",
|
|
120
|
+
envvar="HANDLER_STREAM_READ_TIMEOUT",
|
|
121
|
+
metavar="SECONDS|none",
|
|
122
|
+
help="HTTP read timeout for streaming calls; default is none (env: HANDLER_STREAM_READ_TIMEOUT)",
|
|
123
|
+
)
|
|
61
124
|
@click.pass_context
|
|
62
125
|
def cli(
|
|
63
126
|
ctx: click.Context,
|
|
@@ -65,13 +128,24 @@ def cli(
|
|
|
65
128
|
debug: bool,
|
|
66
129
|
output_format: OutputFormat,
|
|
67
130
|
quiet: bool,
|
|
131
|
+
connect_timeout: str | None,
|
|
132
|
+
read_timeout: str | None,
|
|
133
|
+
write_timeout: str | None,
|
|
134
|
+
pool_timeout: str | None,
|
|
135
|
+
stream_read_timeout: str | None,
|
|
68
136
|
) -> None:
|
|
69
137
|
"""Handler - A2A protocol client CLI."""
|
|
70
|
-
load_runtime_dotenv()
|
|
71
138
|
ctx.ensure_object(dict)
|
|
72
139
|
ctx.obj["output_format"] = output_format
|
|
73
140
|
ctx.obj["quiet"] = quiet
|
|
74
141
|
configure_output(output_format=output_format, quiet=quiet)
|
|
142
|
+
configure_http_timeouts(
|
|
143
|
+
connect_timeout=connect_timeout,
|
|
144
|
+
read_timeout=read_timeout,
|
|
145
|
+
write_timeout=write_timeout,
|
|
146
|
+
pool_timeout=pool_timeout,
|
|
147
|
+
stream_read_timeout=stream_read_timeout,
|
|
148
|
+
)
|
|
75
149
|
|
|
76
150
|
if debug:
|
|
77
151
|
setup_logging(level="DEBUG")
|
|
@@ -103,6 +177,66 @@ def version() -> None:
|
|
|
103
177
|
output.json({"version": __version__})
|
|
104
178
|
|
|
105
179
|
|
|
180
|
+
@cli.command()
|
|
181
|
+
def docs() -> None:
|
|
182
|
+
"""Open the Handler documentation in your browser.
|
|
183
|
+
|
|
184
|
+
\b
|
|
185
|
+
Examples:
|
|
186
|
+
$ handler docs
|
|
187
|
+
"""
|
|
188
|
+
opened = webbrowser.open(DOCS_URL)
|
|
189
|
+
output = Output()
|
|
190
|
+
output.json({"url": DOCS_URL, "opened": opened})
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _upgrade_command() -> list[str] | None:
|
|
194
|
+
"""Return the preferred package manager command for upgrading Handler."""
|
|
195
|
+
if shutil.which("uv"):
|
|
196
|
+
return ["uv", "tool", "upgrade", PACKAGE_NAME]
|
|
197
|
+
if shutil.which("pipx"):
|
|
198
|
+
return ["pipx", "upgrade", PACKAGE_NAME]
|
|
199
|
+
return None
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
@cli.command()
|
|
203
|
+
def update() -> None:
|
|
204
|
+
"""Update Handler to the latest published version.
|
|
205
|
+
|
|
206
|
+
\b
|
|
207
|
+
Examples:
|
|
208
|
+
$ handler update
|
|
209
|
+
$ handler upgrade
|
|
210
|
+
"""
|
|
211
|
+
output = Output()
|
|
212
|
+
command = _upgrade_command()
|
|
213
|
+
if command is None:
|
|
214
|
+
output.error(
|
|
215
|
+
code="installer_not_found",
|
|
216
|
+
message="Could not find uv or pipx to update Handler.",
|
|
217
|
+
suggestion=(
|
|
218
|
+
"Install uv or pipx, then run `uv tool upgrade a2a-handler` "
|
|
219
|
+
"or `pipx upgrade a2a-handler`."
|
|
220
|
+
),
|
|
221
|
+
)
|
|
222
|
+
raise SystemExit(1)
|
|
223
|
+
|
|
224
|
+
result = subprocess.run(command, check=False)
|
|
225
|
+
if result.returncode != 0:
|
|
226
|
+
output.error(
|
|
227
|
+
code="update_failed",
|
|
228
|
+
message="Handler update command failed.",
|
|
229
|
+
details={
|
|
230
|
+
"command": command,
|
|
231
|
+
"returncode": result.returncode,
|
|
232
|
+
},
|
|
233
|
+
)
|
|
234
|
+
raise SystemExit(result.returncode)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
cli.add_command(update, name="upgrade")
|
|
238
|
+
|
|
239
|
+
|
|
106
240
|
@cli.command()
|
|
107
241
|
@click.option(
|
|
108
242
|
"--bearer-env", "-b", help="Env var containing bearer token for agent auth"
|
|
@@ -27,6 +27,13 @@ from a2a_handler.servers import (
|
|
|
27
27
|
)
|
|
28
28
|
|
|
29
29
|
TIMEOUT = 120
|
|
30
|
+
_TIMEOUT_NONE_VALUES = {"none", "null", "off", "disabled"}
|
|
31
|
+
|
|
32
|
+
_connect_timeout: float | None = float(TIMEOUT)
|
|
33
|
+
_read_timeout: float | None = float(TIMEOUT)
|
|
34
|
+
_write_timeout: float | None = float(TIMEOUT)
|
|
35
|
+
_pool_timeout: float | None = float(TIMEOUT)
|
|
36
|
+
_stream_read_timeout: float | None = None
|
|
30
37
|
log = get_logger(__name__)
|
|
31
38
|
|
|
32
39
|
|
|
@@ -39,17 +46,109 @@ class AgentSelection:
|
|
|
39
46
|
|
|
40
47
|
|
|
41
48
|
def build_http_client(
|
|
42
|
-
timeout: int =
|
|
49
|
+
timeout: int | float | httpx.Timeout | None = None,
|
|
43
50
|
credentials: AuthCredentials | None = None,
|
|
44
51
|
) -> httpx.AsyncClient:
|
|
45
52
|
"""Build an HTTP client with the specified timeout."""
|
|
53
|
+
effective_timeout = timeout if timeout is not None else _standard_timeout()
|
|
46
54
|
if credentials and credentials.auth_type == AuthType.MTLS:
|
|
47
55
|
return httpx.AsyncClient(
|
|
48
|
-
timeout=
|
|
56
|
+
timeout=effective_timeout,
|
|
49
57
|
verify=credentials.build_ssl_context(),
|
|
50
58
|
trust_env=False,
|
|
51
59
|
)
|
|
52
|
-
return httpx.AsyncClient(timeout=
|
|
60
|
+
return httpx.AsyncClient(timeout=effective_timeout, trust_env=False)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def build_streaming_http_client(
|
|
64
|
+
credentials: AuthCredentials | None = None,
|
|
65
|
+
) -> httpx.AsyncClient:
|
|
66
|
+
"""Build an HTTP client for long-lived streaming responses.
|
|
67
|
+
|
|
68
|
+
Streaming keeps the usual finite connect/write/pool timeouts but disables
|
|
69
|
+
the read timeout so an active long-running A2A task is not aborted just
|
|
70
|
+
because the server has no SSE event ready for a while.
|
|
71
|
+
"""
|
|
72
|
+
return build_http_client(
|
|
73
|
+
timeout=httpx.Timeout(
|
|
74
|
+
connect=_connect_timeout,
|
|
75
|
+
read=_stream_read_timeout,
|
|
76
|
+
write=_write_timeout,
|
|
77
|
+
pool=_pool_timeout,
|
|
78
|
+
),
|
|
79
|
+
credentials=credentials,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def configure_http_timeouts(
|
|
84
|
+
*,
|
|
85
|
+
connect_timeout: str | int | float | None = None,
|
|
86
|
+
read_timeout: str | int | float | None = None,
|
|
87
|
+
write_timeout: str | int | float | None = None,
|
|
88
|
+
pool_timeout: str | int | float | None = None,
|
|
89
|
+
stream_read_timeout: str | int | float | None = None,
|
|
90
|
+
) -> None:
|
|
91
|
+
"""Configure default CLI HTTP timeouts.
|
|
92
|
+
|
|
93
|
+
Passing no values resets the defaults. String values may be seconds or
|
|
94
|
+
``none`` to disable that timeout.
|
|
95
|
+
"""
|
|
96
|
+
global _connect_timeout, _read_timeout, _write_timeout, _pool_timeout
|
|
97
|
+
global _stream_read_timeout
|
|
98
|
+
|
|
99
|
+
_connect_timeout = _parse_timeout_option(
|
|
100
|
+
connect_timeout, "connect-timeout", float(TIMEOUT)
|
|
101
|
+
)
|
|
102
|
+
_read_timeout = _parse_timeout_option(read_timeout, "read-timeout", float(TIMEOUT))
|
|
103
|
+
_write_timeout = _parse_timeout_option(
|
|
104
|
+
write_timeout, "write-timeout", float(TIMEOUT)
|
|
105
|
+
)
|
|
106
|
+
_pool_timeout = _parse_timeout_option(pool_timeout, "pool-timeout", float(TIMEOUT))
|
|
107
|
+
_stream_read_timeout = _parse_timeout_option(
|
|
108
|
+
stream_read_timeout, "stream-read-timeout", None
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _standard_timeout() -> httpx.Timeout:
|
|
113
|
+
"""Return the configured timeout for regular request/response calls."""
|
|
114
|
+
return httpx.Timeout(
|
|
115
|
+
connect=_connect_timeout,
|
|
116
|
+
read=_read_timeout,
|
|
117
|
+
write=_write_timeout,
|
|
118
|
+
pool=_pool_timeout,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _parse_timeout_option(
|
|
123
|
+
value: str | int | float | None,
|
|
124
|
+
field_name: str,
|
|
125
|
+
default: float | None,
|
|
126
|
+
) -> float | None:
|
|
127
|
+
"""Parse a CLI/env timeout value."""
|
|
128
|
+
if value is None:
|
|
129
|
+
return default
|
|
130
|
+
if isinstance(value, str):
|
|
131
|
+
normalized = value.strip().lower()
|
|
132
|
+
if not normalized:
|
|
133
|
+
return default
|
|
134
|
+
if normalized in _TIMEOUT_NONE_VALUES:
|
|
135
|
+
return None
|
|
136
|
+
try:
|
|
137
|
+
parsed = float(normalized)
|
|
138
|
+
except ValueError as exc:
|
|
139
|
+
raise click.BadParameter(
|
|
140
|
+
"must be a number of seconds or 'none'",
|
|
141
|
+
param_hint=f"--{field_name}",
|
|
142
|
+
) from exc
|
|
143
|
+
else:
|
|
144
|
+
parsed = float(value)
|
|
145
|
+
|
|
146
|
+
if parsed < 0:
|
|
147
|
+
raise click.BadParameter(
|
|
148
|
+
"must be greater than or equal to 0",
|
|
149
|
+
param_hint=f"--{field_name}",
|
|
150
|
+
)
|
|
151
|
+
return parsed
|
|
53
152
|
|
|
54
153
|
|
|
55
154
|
def handle_client_error(e: Exception, agent_url: str, output: Output | None) -> None:
|
|
@@ -11,8 +11,8 @@ from a2a_handler.common.input_validation import InputValidationError, validate_a
|
|
|
11
11
|
from a2a_handler.service import A2AService
|
|
12
12
|
from a2a_handler.validation import (
|
|
13
13
|
ValidationResult,
|
|
14
|
+
ValidationSource,
|
|
14
15
|
validate_agent_card_from_file,
|
|
15
|
-
validate_agent_card_from_url,
|
|
16
16
|
)
|
|
17
17
|
|
|
18
18
|
from ._helpers import (
|
|
@@ -155,7 +155,14 @@ def card_validate(
|
|
|
155
155
|
|
|
156
156
|
async def do_validate() -> None:
|
|
157
157
|
async with build_http_client(credentials=credentials) as http_client:
|
|
158
|
-
|
|
158
|
+
service = A2AService(http_client, resolved_url, credentials=credentials)
|
|
159
|
+
agent_card = await service.get_card()
|
|
160
|
+
result = ValidationResult(
|
|
161
|
+
valid=True,
|
|
162
|
+
source=resolved_url,
|
|
163
|
+
source_type=ValidationSource.URL,
|
|
164
|
+
agent_card=agent_card,
|
|
165
|
+
)
|
|
159
166
|
|
|
160
167
|
_format_validation_result(result, output)
|
|
161
168
|
|