a2a-handler 0.1.26__tar.gz → 0.3.0__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.26 → a2a_handler-0.3.0}/PKG-INFO +48 -6
- {a2a_handler-0.1.26 → a2a_handler-0.3.0}/README.md +44 -2
- {a2a_handler-0.1.26 → a2a_handler-0.3.0}/pyproject.toml +11 -5
- {a2a_handler-0.1.26 → a2a_handler-0.3.0}/pyproject.toml.orig +20 -5
- {a2a_handler-0.1.26 → a2a_handler-0.3.0}/src/a2a_handler/auth.py +98 -11
- {a2a_handler-0.1.26 → a2a_handler-0.3.0}/src/a2a_handler/cli/__init__.py +4 -2
- {a2a_handler-0.1.26 → a2a_handler-0.3.0}/src/a2a_handler/cli/_helpers.py +52 -0
- {a2a_handler-0.1.26 → a2a_handler-0.3.0}/src/a2a_handler/cli/card.py +12 -1
- {a2a_handler-0.1.26 → a2a_handler-0.3.0}/src/a2a_handler/cli/mcp.py +12 -7
- {a2a_handler-0.1.26 → a2a_handler-0.3.0}/src/a2a_handler/cli/message.py +193 -10
- {a2a_handler-0.1.26 → a2a_handler-0.3.0}/src/a2a_handler/cli/server.py +103 -45
- {a2a_handler-0.1.26 → a2a_handler-0.3.0}/src/a2a_handler/cli/task.py +313 -11
- {a2a_handler-0.1.26 → a2a_handler-0.3.0}/src/a2a_handler/common/input_validation.py +53 -0
- {a2a_handler-0.1.26 → a2a_handler-0.3.0}/src/a2a_handler/mcp/server.py +243 -3
- {a2a_handler-0.1.26 → a2a_handler-0.3.0}/src/a2a_handler/servers.py +94 -54
- a2a_handler-0.3.0/src/a2a_handler/service.py +1841 -0
- {a2a_handler-0.1.26 → a2a_handler-0.3.0}/src/a2a_handler/tui/app.py +10 -2
- {a2a_handler-0.1.26 → a2a_handler-0.3.0}/src/a2a_handler/tui/app.tcss +37 -0
- {a2a_handler-0.1.26 → a2a_handler-0.3.0}/src/a2a_handler/tui/commands/palette.py +5 -0
- {a2a_handler-0.1.26 → a2a_handler-0.3.0}/src/a2a_handler/tui/components/auth.py +115 -0
- {a2a_handler-0.1.26 → a2a_handler-0.3.0}/src/a2a_handler/tui/components/input.py +45 -2
- {a2a_handler-0.1.26 → a2a_handler-0.3.0}/src/a2a_handler/tui/components/messages.py +129 -4
- {a2a_handler-0.1.26 → a2a_handler-0.3.0}/src/a2a_handler/tui/server/save.py +18 -4
- {a2a_handler-0.1.26 → a2a_handler-0.3.0}/src/a2a_handler/tui/server/tab.py +376 -283
- {a2a_handler-0.1.26 → a2a_handler-0.3.0}/src/a2a_handler/tui/server/views.py +4 -4
- a2a_handler-0.3.0/src/a2a_handler/turn.py +335 -0
- {a2a_handler-0.1.26 → a2a_handler-0.3.0}/src/a2a_handler/validation.py +44 -77
- {a2a_handler-0.1.26 → a2a_handler-0.3.0}/src/a2a_handler/webhook.py +80 -19
- a2a_handler-0.1.26/src/a2a_handler/server/__init__.py +0 -79
- a2a_handler-0.1.26/src/a2a_handler/server/agent.py +0 -133
- a2a_handler-0.1.26/src/a2a_handler/server/app.py +0 -221
- a2a_handler-0.1.26/src/a2a_handler/server/card.py +0 -164
- a2a_handler-0.1.26/src/a2a_handler/server/ollama.py +0 -75
- a2a_handler-0.1.26/src/a2a_handler/server/tools/__init__.py +0 -23
- a2a_handler-0.1.26/src/a2a_handler/server/tools/a2a_docs.py +0 -140
- a2a_handler-0.1.26/src/a2a_handler/server/tools/handler_docs.py +0 -44
- a2a_handler-0.1.26/src/a2a_handler/server/tools/source.py +0 -109
- a2a_handler-0.1.26/src/a2a_handler/service.py +0 -745
- {a2a_handler-0.1.26 → a2a_handler-0.3.0}/LICENSE +0 -0
- {a2a_handler-0.1.26 → a2a_handler-0.3.0}/src/a2a_handler/__init__.py +0 -0
- {a2a_handler-0.1.26 → a2a_handler-0.3.0}/src/a2a_handler/cli/schema.py +0 -0
- {a2a_handler-0.1.26 → a2a_handler-0.3.0}/src/a2a_handler/cli/session.py +0 -0
- {a2a_handler-0.1.26 → a2a_handler-0.3.0}/src/a2a_handler/common/__init__.py +0 -0
- {a2a_handler-0.1.26 → a2a_handler-0.3.0}/src/a2a_handler/common/config.py +0 -0
- {a2a_handler-0.1.26 → a2a_handler-0.3.0}/src/a2a_handler/common/dotenv.py +0 -0
- {a2a_handler-0.1.26 → a2a_handler-0.3.0}/src/a2a_handler/common/logging.py +0 -0
- {a2a_handler-0.1.26 → a2a_handler-0.3.0}/src/a2a_handler/common/output.py +0 -0
- {a2a_handler-0.1.26 → a2a_handler-0.3.0}/src/a2a_handler/mcp/__init__.py +0 -0
- {a2a_handler-0.1.26 → a2a_handler-0.3.0}/src/a2a_handler/session.py +0 -0
- {a2a_handler-0.1.26 → a2a_handler-0.3.0}/src/a2a_handler/tui/__init__.py +0 -0
- {a2a_handler-0.1.26 → a2a_handler-0.3.0}/src/a2a_handler/tui/commands/__init__.py +0 -0
- {a2a_handler-0.1.26 → a2a_handler-0.3.0}/src/a2a_handler/tui/commands/screens.py +0 -0
- {a2a_handler-0.1.26 → a2a_handler-0.3.0}/src/a2a_handler/tui/components/__init__.py +0 -0
- {a2a_handler-0.1.26 → a2a_handler-0.3.0}/src/a2a_handler/tui/components/artifacts.py +0 -0
- {a2a_handler-0.1.26 → a2a_handler-0.3.0}/src/a2a_handler/tui/components/card.py +0 -0
- {a2a_handler-0.1.26 → a2a_handler-0.3.0}/src/a2a_handler/tui/components/headers.py +0 -0
- {a2a_handler-0.1.26 → a2a_handler-0.3.0}/src/a2a_handler/tui/components/logs.py +0 -0
- {a2a_handler-0.1.26 → a2a_handler-0.3.0}/src/a2a_handler/tui/components/tasks.py +0 -0
- {a2a_handler-0.1.26 → a2a_handler-0.3.0}/src/a2a_handler/tui/server/__init__.py +0 -0
- {a2a_handler-0.1.26 → a2a_handler-0.3.0}/src/a2a_handler/tui/server/session.py +0 -0
- {a2a_handler-0.1.26 → a2a_handler-0.3.0}/src/a2a_handler/tui/server/tabs.py +0 -0
- {a2a_handler-0.1.26 → a2a_handler-0.3.0}/src/a2a_handler/tui/server/types.py +0 -0
- {a2a_handler-0.1.26 → a2a_handler-0.3.0}/src/a2a_handler/tui/server/workspace.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: a2a-handler
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
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
|
|
@@ -10,7 +10,6 @@ License-File: LICENSE
|
|
|
10
10
|
Classifier: Development Status :: 4 - Beta
|
|
11
11
|
Classifier: Environment :: Console
|
|
12
12
|
Classifier: Intended Audience :: Developers
|
|
13
|
-
Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
|
|
14
13
|
Classifier: Operating System :: OS Independent
|
|
15
14
|
Classifier: Programming Language :: Python :: 3
|
|
16
15
|
Classifier: Programming Language :: Python :: 3.11
|
|
@@ -20,23 +19,24 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
|
20
19
|
Classifier: Typing :: Typed
|
|
21
20
|
Requires-Dist: a2a-sdk[http-server]>=1.0.0,<2.0.0
|
|
22
21
|
Requires-Dist: click>=8.0.0
|
|
23
|
-
Requires-Dist: google-adk[mcp]>=2.5.0
|
|
24
22
|
Requires-Dist: httpx>=0.27.0
|
|
25
|
-
Requires-Dist: litellm>=1.0.0
|
|
26
23
|
Requires-Dist: python-dotenv>=1.0.0
|
|
27
24
|
Requires-Dist: rich>=13.0.0
|
|
25
|
+
Requires-Dist: starlette>=0.37.0
|
|
28
26
|
Requires-Dist: textual>=0.47.0
|
|
29
27
|
Requires-Dist: uvicorn>=0.30.0
|
|
30
28
|
Requires-Dist: textual-serve>=1.1.0
|
|
31
29
|
Requires-Dist: mcp[cli]>=1.27.1,<2
|
|
32
30
|
Requires-Dist: truststore>=0.9.0
|
|
33
31
|
Requires-Dist: platformdirs>=4.0.0
|
|
32
|
+
Requires-Dist: a2a-sdk[grpc]>=1.0.0,<2.0.0 ; extra == 'grpc'
|
|
34
33
|
Requires-Python: >=3.11
|
|
35
34
|
Project-URL: Homepage, https://github.com/alDuncanson/handler
|
|
36
35
|
Project-URL: Repository, https://github.com/alDuncanson/handler
|
|
37
36
|
Project-URL: Documentation, https://github.com/alDuncanson/handler/tree/main/docs
|
|
38
37
|
Project-URL: Issues, https://github.com/alDuncanson/handler/issues
|
|
39
38
|
Project-URL: Changelog, https://github.com/alDuncanson/handler/releases
|
|
39
|
+
Provides-Extra: grpc
|
|
40
40
|
Description-Content-Type: text/markdown
|
|
41
41
|
|
|
42
42
|
# Handler
|
|
@@ -53,9 +53,24 @@ client for software engineers building, testing, and operating agentic systems.
|
|
|
53
53
|
It provides an interactive TUI, a scriptable CLI with structured output, and an
|
|
54
54
|
MCP server that lets other agents integrate with A2A services directly. Handler
|
|
55
55
|
also supports global and repo-scoped A2A server configuration with bearer,
|
|
56
|
-
API key, mTLS,
|
|
56
|
+
API key, HTTP Basic, mTLS, OAuth2 client credentials, and OpenID Connect auth.
|
|
57
57
|
|
|
58
|
-

|
|
59
|
+
|
|
60
|
+
## Features
|
|
61
|
+
|
|
62
|
+
- Streams replies as they arrive, in the TUI and the CLI, and can stop a
|
|
63
|
+
running task or answer an agent that pauses for input
|
|
64
|
+
- Sends text, files (inline or by URL), and structured data, and shows the
|
|
65
|
+
file and data parts agents send back
|
|
66
|
+
- Lists, inspects, cancels, and resubscribes to tasks, and manages push
|
|
67
|
+
notification configs with a bundled local webhook receiver
|
|
68
|
+
- Speaks JSON-RPC and HTTP+JSON out of the box, gRPC with the `grpc` extra,
|
|
69
|
+
and lets the agent card pick the transport
|
|
70
|
+
- Requests A2A extensions and fetches the extended card an agent offers to
|
|
71
|
+
authenticated clients
|
|
72
|
+
- Keeps credentials on the origin you configured, never on a host an agent
|
|
73
|
+
card happens to name
|
|
59
74
|
|
|
60
75
|
## Install
|
|
61
76
|
|
|
@@ -97,6 +112,18 @@ Send a message from the CLI:
|
|
|
97
112
|
handler message send --url URL --text "hello"
|
|
98
113
|
```
|
|
99
114
|
|
|
115
|
+
Stream the reply, with a file attached:
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
handler message stream --url URL --text "Review this" --file ./report.pdf
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
List the agent's tasks:
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
handler task list --url URL
|
|
125
|
+
```
|
|
126
|
+
|
|
100
127
|
Open the full documentation:
|
|
101
128
|
|
|
102
129
|
```bash
|
|
@@ -120,3 +147,18 @@ pipx run a2a-handler
|
|
|
120
147
|
## Documentation
|
|
121
148
|
|
|
122
149
|
Read the documentation at <https://handler.alduncanson.com>.
|
|
150
|
+
|
|
151
|
+
### For coding agents
|
|
152
|
+
|
|
153
|
+
The docs ship as an agent skill, regenerated on every deploy. Install it into
|
|
154
|
+
Claude Code, Cursor, Windsurf, or any agent that reads the `SKILL.md` format:
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
npx skills add https://handler.alduncanson.com
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Or fetch it by hand from <https://handler.alduncanson.com/skill.md>. For live
|
|
161
|
+
lookups, agents can also use the hosted documentation MCP server at
|
|
162
|
+
<https://handler.alduncanson.com/mcp> or the page index at
|
|
163
|
+
<https://handler.alduncanson.com/llms.txt>. See the
|
|
164
|
+
[agent skill guide](https://handler.alduncanson.com/guides/agent-skill).
|
|
@@ -12,9 +12,24 @@ client for software engineers building, testing, and operating agentic systems.
|
|
|
12
12
|
It provides an interactive TUI, a scriptable CLI with structured output, and an
|
|
13
13
|
MCP server that lets other agents integrate with A2A services directly. Handler
|
|
14
14
|
also supports global and repo-scoped A2A server configuration with bearer,
|
|
15
|
-
API key, mTLS,
|
|
15
|
+
API key, HTTP Basic, mTLS, OAuth2 client credentials, and OpenID Connect auth.
|
|
16
16
|
|
|
17
|
-

|
|
18
|
+
|
|
19
|
+
## Features
|
|
20
|
+
|
|
21
|
+
- Streams replies as they arrive, in the TUI and the CLI, and can stop a
|
|
22
|
+
running task or answer an agent that pauses for input
|
|
23
|
+
- Sends text, files (inline or by URL), and structured data, and shows the
|
|
24
|
+
file and data parts agents send back
|
|
25
|
+
- Lists, inspects, cancels, and resubscribes to tasks, and manages push
|
|
26
|
+
notification configs with a bundled local webhook receiver
|
|
27
|
+
- Speaks JSON-RPC and HTTP+JSON out of the box, gRPC with the `grpc` extra,
|
|
28
|
+
and lets the agent card pick the transport
|
|
29
|
+
- Requests A2A extensions and fetches the extended card an agent offers to
|
|
30
|
+
authenticated clients
|
|
31
|
+
- Keeps credentials on the origin you configured, never on a host an agent
|
|
32
|
+
card happens to name
|
|
18
33
|
|
|
19
34
|
## Install
|
|
20
35
|
|
|
@@ -56,6 +71,18 @@ Send a message from the CLI:
|
|
|
56
71
|
handler message send --url URL --text "hello"
|
|
57
72
|
```
|
|
58
73
|
|
|
74
|
+
Stream the reply, with a file attached:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
handler message stream --url URL --text "Review this" --file ./report.pdf
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
List the agent's tasks:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
handler task list --url URL
|
|
84
|
+
```
|
|
85
|
+
|
|
59
86
|
Open the full documentation:
|
|
60
87
|
|
|
61
88
|
```bash
|
|
@@ -79,3 +106,18 @@ pipx run a2a-handler
|
|
|
79
106
|
## Documentation
|
|
80
107
|
|
|
81
108
|
Read the documentation at <https://handler.alduncanson.com>.
|
|
109
|
+
|
|
110
|
+
### For coding agents
|
|
111
|
+
|
|
112
|
+
The docs ship as an agent skill, regenerated on every deploy. Install it into
|
|
113
|
+
Claude Code, Cursor, Windsurf, or any agent that reads the `SKILL.md` format:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
npx skills add https://handler.alduncanson.com
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Or fetch it by hand from <https://handler.alduncanson.com/skill.md>. For live
|
|
120
|
+
lookups, agents can also use the hosted documentation MCP server at
|
|
121
|
+
<https://handler.alduncanson.com/mcp> or the page index at
|
|
122
|
+
<https://handler.alduncanson.com/llms.txt>. See the
|
|
123
|
+
[agent skill guide](https://handler.alduncanson.com/guides/agent-skill).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "a2a-handler"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.3.0"
|
|
4
4
|
description = "An A2A Protocol client TUI and CLI."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.11"
|
|
@@ -19,7 +19,6 @@ classifiers = [
|
|
|
19
19
|
"Development Status :: 4 - Beta",
|
|
20
20
|
"Environment :: Console",
|
|
21
21
|
"Intended Audience :: Developers",
|
|
22
|
-
"License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
|
|
23
22
|
"Operating System :: OS Independent",
|
|
24
23
|
"Programming Language :: Python :: 3",
|
|
25
24
|
"Programming Language :: Python :: 3.11",
|
|
@@ -31,11 +30,10 @@ classifiers = [
|
|
|
31
30
|
dependencies = [
|
|
32
31
|
"a2a-sdk[http-server]>=1.0.0,<2.0.0",
|
|
33
32
|
"click>=8.0.0",
|
|
34
|
-
"google-adk[mcp]>=2.5.0",
|
|
35
33
|
"httpx>=0.27.0",
|
|
36
|
-
"litellm>=1.0.0",
|
|
37
34
|
"python-dotenv>=1.0.0",
|
|
38
35
|
"rich>=13.0.0",
|
|
36
|
+
"starlette>=0.37.0",
|
|
39
37
|
"textual>=0.47.0",
|
|
40
38
|
"uvicorn>=0.30.0",
|
|
41
39
|
"textual-serve>=1.1.0",
|
|
@@ -48,6 +46,9 @@ dependencies = [
|
|
|
48
46
|
name = "Al Duncanson"
|
|
49
47
|
email = "alDuncanson@proton.me"
|
|
50
48
|
|
|
49
|
+
[project.optional-dependencies]
|
|
50
|
+
grpc = ["a2a-sdk[grpc]>=1.0.0,<2.0.0"]
|
|
51
|
+
|
|
51
52
|
[project.urls]
|
|
52
53
|
Homepage = "https://github.com/alDuncanson/handler"
|
|
53
54
|
Repository = "https://github.com/alDuncanson/handler"
|
|
@@ -70,12 +71,17 @@ dev = [
|
|
|
70
71
|
]
|
|
71
72
|
|
|
72
73
|
[build-system]
|
|
73
|
-
requires = ["uv_build>=0.9.15,<0.
|
|
74
|
+
requires = ["uv_build>=0.9.15,<0.13.0"]
|
|
74
75
|
build-backend = "uv_build"
|
|
75
76
|
|
|
76
77
|
[tool.uv.build-backend]
|
|
77
78
|
module-root = "src"
|
|
78
79
|
|
|
80
|
+
[[tool.uv.index]]
|
|
81
|
+
name = "pypi"
|
|
82
|
+
url = "https://pypi.org/simple"
|
|
83
|
+
default = true
|
|
84
|
+
|
|
79
85
|
[tool.pytest.ini_options]
|
|
80
86
|
asyncio_mode = "auto"
|
|
81
87
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "a2a-handler"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.3.0"
|
|
4
4
|
description = "An A2A Protocol client TUI and CLI."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.11"
|
|
@@ -22,7 +22,6 @@ classifiers = [
|
|
|
22
22
|
"Development Status :: 4 - Beta",
|
|
23
23
|
"Environment :: Console",
|
|
24
24
|
"Intended Audience :: Developers",
|
|
25
|
-
"License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
|
|
26
25
|
"Operating System :: OS Independent",
|
|
27
26
|
"Programming Language :: Python :: 3",
|
|
28
27
|
"Programming Language :: Python :: 3.11",
|
|
@@ -34,11 +33,12 @@ classifiers = [
|
|
|
34
33
|
dependencies = [
|
|
35
34
|
"a2a-sdk[http-server]>=1.0.0,<2.0.0",
|
|
36
35
|
"click>=8.0.0",
|
|
37
|
-
"google-adk[mcp]>=2.5.0",
|
|
38
36
|
"httpx>=0.27.0",
|
|
39
|
-
"litellm>=1.0.0",
|
|
40
37
|
"python-dotenv>=1.0.0",
|
|
41
38
|
"rich>=13.0.0",
|
|
39
|
+
# Imported directly by the push notification webhook server. Declared rather
|
|
40
|
+
# than relied upon transitively via a2a-sdk[http-server].
|
|
41
|
+
"starlette>=0.37.0",
|
|
42
42
|
"textual>=0.47.0",
|
|
43
43
|
"uvicorn>=0.30.0",
|
|
44
44
|
"textual-serve>=1.1.0",
|
|
@@ -47,6 +47,12 @@ dependencies = [
|
|
|
47
47
|
"platformdirs>=4.0.0",
|
|
48
48
|
]
|
|
49
49
|
|
|
50
|
+
[project.optional-dependencies]
|
|
51
|
+
# Talking to gRPC-only agents needs the SDK's gRPC transport and its
|
|
52
|
+
# grpcio dependencies, which are heavy enough to keep out of the default
|
|
53
|
+
# install.
|
|
54
|
+
grpc = ["a2a-sdk[grpc]>=1.0.0,<2.0.0"]
|
|
55
|
+
|
|
50
56
|
[dependency-groups]
|
|
51
57
|
dev = [
|
|
52
58
|
"ruff>=0.8.0",
|
|
@@ -69,12 +75,21 @@ Changelog = "https://github.com/alDuncanson/handler/releases"
|
|
|
69
75
|
handler = "a2a_handler.cli:main"
|
|
70
76
|
|
|
71
77
|
[build-system]
|
|
72
|
-
requires = ["uv_build>=0.9.15,<0.
|
|
78
|
+
requires = ["uv_build>=0.9.15,<0.13.0"]
|
|
73
79
|
build-backend = "uv_build"
|
|
74
80
|
|
|
75
81
|
[tool.uv.build-backend]
|
|
76
82
|
module-root = "src"
|
|
77
83
|
|
|
84
|
+
# This is a public package published to PyPI, so its lockfile must resolve from
|
|
85
|
+
# pypi.org. Pinned explicitly because an ambient UV_INDEX_URL (e.g. a corporate
|
|
86
|
+
# mirror) would otherwise rewrite every source URL in uv.lock on the next
|
|
87
|
+
# `uv lock`, leaking internal infrastructure and breaking CI for contributors.
|
|
88
|
+
[[tool.uv.index]]
|
|
89
|
+
name = "pypi"
|
|
90
|
+
url = "https://pypi.org/simple"
|
|
91
|
+
default = true
|
|
92
|
+
|
|
78
93
|
[tool.pytest.ini_options]
|
|
79
94
|
asyncio_mode = "auto"
|
|
80
95
|
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
"""Authentication support for A2A protocol.
|
|
2
2
|
|
|
3
3
|
Handles credential storage and HTTP authentication header generation.
|
|
4
|
-
Supports API key, HTTP bearer, mTLS (mutual TLS),
|
|
5
|
-
authentication schemes.
|
|
4
|
+
Supports API key, HTTP bearer, HTTP basic, mTLS (mutual TLS), OAuth2 client
|
|
5
|
+
credentials, and OpenID Connect authentication schemes.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
|
+
import base64
|
|
10
11
|
import ssl
|
|
11
12
|
import time
|
|
12
13
|
from dataclasses import dataclass, field
|
|
@@ -30,9 +31,18 @@ class AuthType(str, Enum):
|
|
|
30
31
|
"""Supported authentication types."""
|
|
31
32
|
|
|
32
33
|
API_KEY = "api_key"
|
|
34
|
+
BASIC = "basic"
|
|
33
35
|
BEARER = "bearer"
|
|
34
36
|
MTLS = "mtls"
|
|
35
37
|
OAUTH2 = "oauth2"
|
|
38
|
+
OIDC = "oidc"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
#: Auth types whose access token is fetched from a token endpoint and cached.
|
|
42
|
+
TOKEN_FETCHING_AUTH_TYPES = {AuthType.OAUTH2, AuthType.OIDC}
|
|
43
|
+
|
|
44
|
+
#: Path of the OpenID Connect discovery document below the issuer URL.
|
|
45
|
+
OIDC_DISCOVERY_PATH = "/.well-known/openid-configuration"
|
|
36
46
|
|
|
37
47
|
|
|
38
48
|
@dataclass
|
|
@@ -45,13 +55,15 @@ class AuthCredentials:
|
|
|
45
55
|
auth_type: AuthType
|
|
46
56
|
value: str = ""
|
|
47
57
|
header_name: str | None = None # For API key: custom header name
|
|
58
|
+
username: str | None = None # For HTTP basic: username (value is password)
|
|
48
59
|
cert_path: str | None = None # For mTLS: client certificate path
|
|
49
60
|
key_path: str | None = None # For mTLS: client private key path
|
|
50
61
|
ca_cert_path: str | None = None # For mTLS: CA certificate path
|
|
51
|
-
token_url: str | None = None # For OAuth2: token endpoint
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
62
|
+
token_url: str | None = None # For OAuth2/OIDC: token endpoint
|
|
63
|
+
issuer_url: str | None = None # For OIDC: issuer to discover endpoints from
|
|
64
|
+
client_id: str | None = None # For OAuth2/OIDC: client ID
|
|
65
|
+
client_secret: str | None = None # For OAuth2/OIDC: client secret
|
|
66
|
+
scopes: list[str] | None = None # For OAuth2/OIDC: optional scopes
|
|
55
67
|
custom_headers: dict[str, str] | None = None # Additional headers for any auth type
|
|
56
68
|
_token_expires_at: float | None = field(default=None, repr=False)
|
|
57
69
|
|
|
@@ -64,8 +76,9 @@ class AuthCredentials:
|
|
|
64
76
|
"key_path": self.key_path,
|
|
65
77
|
"ca_cert_path": self.ca_cert_path,
|
|
66
78
|
"token_url": self.token_url,
|
|
79
|
+
"issuer_url": self.issuer_url,
|
|
67
80
|
}
|
|
68
|
-
for secret_field in ("value", "client_id", "client_secret"):
|
|
81
|
+
for secret_field in ("value", "username", "client_id", "client_secret"):
|
|
69
82
|
val = getattr(self, secret_field)
|
|
70
83
|
redacted[secret_field] = "***" if val else None
|
|
71
84
|
if self.custom_headers:
|
|
@@ -83,8 +96,12 @@ class AuthCredentials:
|
|
|
83
96
|
headers.update(self.custom_headers)
|
|
84
97
|
if self.auth_type == AuthType.BEARER and self.value:
|
|
85
98
|
headers["Authorization"] = f"Bearer {self.value}"
|
|
86
|
-
elif self.auth_type
|
|
99
|
+
elif self.auth_type in TOKEN_FETCHING_AUTH_TYPES and self.value:
|
|
87
100
|
headers["Authorization"] = f"Bearer {self.value}"
|
|
101
|
+
elif self.auth_type == AuthType.BASIC and self.username is not None:
|
|
102
|
+
userpass = f"{self.username}:{self.value}".encode()
|
|
103
|
+
encoded = base64.b64encode(userpass).decode("ascii")
|
|
104
|
+
headers["Authorization"] = f"Basic {encoded}"
|
|
88
105
|
elif self.auth_type == AuthType.API_KEY:
|
|
89
106
|
header = self.header_name or "X-API-Key"
|
|
90
107
|
headers[header] = self.value
|
|
@@ -122,8 +139,39 @@ class AuthCredentials:
|
|
|
122
139
|
self.value = ""
|
|
123
140
|
self._token_expires_at = None
|
|
124
141
|
|
|
142
|
+
async def discover_oidc_token_endpoint(self) -> str:
|
|
143
|
+
"""Resolve the token endpoint from the OIDC discovery document.
|
|
144
|
+
|
|
145
|
+
Fetches ``<issuer>/.well-known/openid-configuration`` and caches the
|
|
146
|
+
advertised ``token_endpoint`` on ``token_url``.
|
|
147
|
+
"""
|
|
148
|
+
if self.auth_type != AuthType.OIDC:
|
|
149
|
+
raise ValueError("OIDC discovery is only valid for OIDC credentials")
|
|
150
|
+
if self.token_url:
|
|
151
|
+
return self.token_url
|
|
152
|
+
if not self.issuer_url:
|
|
153
|
+
raise ValueError("issuer_url is required for OIDC discovery")
|
|
154
|
+
validate_token_url(self.issuer_url)
|
|
155
|
+
|
|
156
|
+
discovery_url = self.issuer_url.rstrip("/") + OIDC_DISCOVERY_PATH
|
|
157
|
+
async with httpx.AsyncClient(trust_env=False) as discovery_client:
|
|
158
|
+
response = await discovery_client.get(discovery_url)
|
|
159
|
+
response.raise_for_status()
|
|
160
|
+
document = response.json()
|
|
161
|
+
token_endpoint = document.get("token_endpoint")
|
|
162
|
+
if not isinstance(token_endpoint, str) or not token_endpoint:
|
|
163
|
+
raise ValueError(
|
|
164
|
+
f"OIDC discovery document at {discovery_url} has no token_endpoint"
|
|
165
|
+
)
|
|
166
|
+
validate_token_url(token_endpoint)
|
|
167
|
+
self.token_url = token_endpoint
|
|
168
|
+
return token_endpoint
|
|
169
|
+
|
|
125
170
|
async def fetch_oauth2_token(self) -> str:
|
|
126
|
-
"""Fetch an access token using
|
|
171
|
+
"""Fetch an access token using the client credentials grant.
|
|
172
|
+
|
|
173
|
+
Valid for OAuth2 and OIDC credentials; OIDC resolves its token
|
|
174
|
+
endpoint through the issuer's discovery document first.
|
|
127
175
|
|
|
128
176
|
Uses a short-lived HTTP client so that agent auth headers and custom
|
|
129
177
|
headers on the main client are never sent to the token endpoint.
|
|
@@ -131,8 +179,10 @@ class AuthCredentials:
|
|
|
131
179
|
Updates self.value with the new token and returns it.
|
|
132
180
|
Parses ``expires_in`` from the token response to track expiry.
|
|
133
181
|
"""
|
|
134
|
-
if self.auth_type
|
|
135
|
-
raise ValueError("Token fetch is only valid for OAuth2 credentials")
|
|
182
|
+
if self.auth_type not in TOKEN_FETCHING_AUTH_TYPES:
|
|
183
|
+
raise ValueError("Token fetch is only valid for OAuth2 or OIDC credentials")
|
|
184
|
+
if self.auth_type == AuthType.OIDC:
|
|
185
|
+
await self.discover_oidc_token_endpoint()
|
|
136
186
|
if not self.token_url or not self.client_id or not self.client_secret:
|
|
137
187
|
raise ValueError("token_url, client_id, and client_secret are required")
|
|
138
188
|
validate_token_url(self.token_url)
|
|
@@ -168,6 +218,10 @@ class AuthCredentials:
|
|
|
168
218
|
"value": self.value,
|
|
169
219
|
"header_name": self.header_name,
|
|
170
220
|
}
|
|
221
|
+
if self.username is not None:
|
|
222
|
+
data["username"] = self.username
|
|
223
|
+
if self.issuer_url:
|
|
224
|
+
data["issuer_url"] = self.issuer_url
|
|
171
225
|
if self.cert_path:
|
|
172
226
|
data["cert_path"] = self.cert_path
|
|
173
227
|
if self.key_path:
|
|
@@ -197,6 +251,8 @@ class AuthCredentials:
|
|
|
197
251
|
auth_type=AuthType(data["auth_type"]),
|
|
198
252
|
value=data.get("value") or "",
|
|
199
253
|
header_name=data.get("header_name"),
|
|
254
|
+
username=data.get("username"),
|
|
255
|
+
issuer_url=data.get("issuer_url"),
|
|
200
256
|
cert_path=data.get("cert_path"),
|
|
201
257
|
key_path=data.get("key_path"),
|
|
202
258
|
ca_cert_path=data.get("ca_cert_path"),
|
|
@@ -226,6 +282,15 @@ def create_bearer_auth(token: str) -> AuthCredentials:
|
|
|
226
282
|
return AuthCredentials(auth_type=AuthType.BEARER, value=token)
|
|
227
283
|
|
|
228
284
|
|
|
285
|
+
def create_basic_auth(username: str, password: str) -> AuthCredentials:
|
|
286
|
+
"""Create HTTP basic authentication."""
|
|
287
|
+
return AuthCredentials(
|
|
288
|
+
auth_type=AuthType.BASIC,
|
|
289
|
+
username=username,
|
|
290
|
+
value=password,
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
|
|
229
294
|
def create_api_key_auth(
|
|
230
295
|
key: str,
|
|
231
296
|
header_name: str = "X-API-Key",
|
|
@@ -286,3 +351,25 @@ def create_oauth2_auth(
|
|
|
286
351
|
client_secret=client_secret,
|
|
287
352
|
scopes=scopes,
|
|
288
353
|
)
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def create_oidc_auth(
|
|
357
|
+
issuer_url: str,
|
|
358
|
+
client_id: str,
|
|
359
|
+
client_secret: str,
|
|
360
|
+
scopes: list[str] | None = None,
|
|
361
|
+
access_token: str = "",
|
|
362
|
+
) -> AuthCredentials:
|
|
363
|
+
"""Create OpenID Connect authentication.
|
|
364
|
+
|
|
365
|
+
The token endpoint is discovered from the issuer's
|
|
366
|
+
``/.well-known/openid-configuration`` document on first token fetch.
|
|
367
|
+
"""
|
|
368
|
+
return AuthCredentials(
|
|
369
|
+
auth_type=AuthType.OIDC,
|
|
370
|
+
value=access_token,
|
|
371
|
+
issuer_url=issuer_url,
|
|
372
|
+
client_id=client_id,
|
|
373
|
+
client_secret=client_secret,
|
|
374
|
+
scopes=scopes,
|
|
375
|
+
)
|
|
@@ -6,7 +6,7 @@ Provides commands for interacting with A2A agents:
|
|
|
6
6
|
- task notification set: Configure push notifications
|
|
7
7
|
- card get/validate: Agent card operations
|
|
8
8
|
- server list/show/add/remove/validate: Manage configured servers
|
|
9
|
-
- server run
|
|
9
|
+
- server run push: Run the local push notification webhook server
|
|
10
10
|
- session list/show/clear: Manage saved conversation sessions
|
|
11
11
|
|
|
12
12
|
Authentication is configured per-server via `handler server add` or
|
|
@@ -151,8 +151,10 @@ def cli(
|
|
|
151
151
|
setup_logging(level="DEBUG")
|
|
152
152
|
elif verbose:
|
|
153
153
|
setup_logging(level="INFO")
|
|
154
|
-
|
|
154
|
+
elif quiet:
|
|
155
155
|
setup_logging(level="ERROR")
|
|
156
|
+
else:
|
|
157
|
+
setup_logging(level="WARNING")
|
|
156
158
|
|
|
157
159
|
|
|
158
160
|
cli.add_command(message)
|
|
@@ -11,6 +11,13 @@ from a2a.client.errors import (
|
|
|
11
11
|
A2AClientTimeoutError,
|
|
12
12
|
AgentCardResolutionError,
|
|
13
13
|
)
|
|
14
|
+
from a2a.utils.errors import (
|
|
15
|
+
A2AError,
|
|
16
|
+
MethodNotFoundError,
|
|
17
|
+
TaskNotCancelableError,
|
|
18
|
+
TaskNotFoundError,
|
|
19
|
+
UnsupportedOperationError,
|
|
20
|
+
)
|
|
14
21
|
|
|
15
22
|
from a2a_handler.auth import (
|
|
16
23
|
AuthCredentials,
|
|
@@ -172,6 +179,14 @@ def handle_client_error(e: Exception, agent_url: str, output: Output | None) ->
|
|
|
172
179
|
log.error("A2A client error: %s", e)
|
|
173
180
|
message = str(e)
|
|
174
181
|
error_code = "a2a_client_error"
|
|
182
|
+
elif isinstance(e, A2AError):
|
|
183
|
+
# The agent answered with a protocol error. That is a normal outcome
|
|
184
|
+
# with a clear meaning, not a crash, so no traceback.
|
|
185
|
+
log.error("Agent returned an error: %s", e)
|
|
186
|
+
message = str(e)
|
|
187
|
+
error_code = "a2a_error"
|
|
188
|
+
details = {"error": type(e).__name__, "agent_url": agent_url}
|
|
189
|
+
suggestion = _protocol_error_suggestion(e)
|
|
175
190
|
elif isinstance(e, httpx.ConnectError):
|
|
176
191
|
log.error("Connection refused to %s", agent_url)
|
|
177
192
|
message = f"Connection refused: Is the server running at {agent_url}?"
|
|
@@ -206,6 +221,20 @@ def handle_client_error(e: Exception, agent_url: str, output: Output | None) ->
|
|
|
206
221
|
print(f"Error [{error_code}]: {message}", file=sys.stderr)
|
|
207
222
|
|
|
208
223
|
|
|
224
|
+
def _protocol_error_suggestion(error: A2AError) -> str | None:
|
|
225
|
+
"""A next step for the protocol errors a user can act on."""
|
|
226
|
+
if isinstance(error, TaskNotCancelableError):
|
|
227
|
+
return (
|
|
228
|
+
"The task has probably already finished; `handler task get` shows "
|
|
229
|
+
"its current state"
|
|
230
|
+
)
|
|
231
|
+
if isinstance(error, TaskNotFoundError):
|
|
232
|
+
return "Check the task ID; the agent may have discarded the task"
|
|
233
|
+
if isinstance(error, (UnsupportedOperationError, MethodNotFoundError)):
|
|
234
|
+
return "This agent does not implement that operation"
|
|
235
|
+
return None
|
|
236
|
+
|
|
237
|
+
|
|
209
238
|
def _resolve_env_secret(env_var: str, label: str) -> str:
|
|
210
239
|
"""Read a secret from an environment variable."""
|
|
211
240
|
value = os.environ.get(env_var)
|
|
@@ -292,6 +321,29 @@ def resolve_agent_target(
|
|
|
292
321
|
return selection.agent_url, credentials
|
|
293
322
|
|
|
294
323
|
|
|
324
|
+
def resolve_task_id(positional: str | None, option: str | None) -> str | None:
|
|
325
|
+
"""Resolve a task ID given either positionally or via --task.
|
|
326
|
+
|
|
327
|
+
Returns None when neither is supplied, because `--params` may still carry a
|
|
328
|
+
task_id; callers check for a missing ID after merging that payload.
|
|
329
|
+
"""
|
|
330
|
+
if positional is not None and option is not None and positional != option:
|
|
331
|
+
raise click.UsageError(
|
|
332
|
+
"Conflicting task IDs: "
|
|
333
|
+
f"{positional!r} was given positionally and {option!r} with --task."
|
|
334
|
+
)
|
|
335
|
+
return positional if positional is not None else option
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def require_task_id(task_id: str | None) -> str:
|
|
339
|
+
"""Return the resolved task ID, or fail with usage guidance when absent."""
|
|
340
|
+
if not task_id:
|
|
341
|
+
raise click.UsageError(
|
|
342
|
+
"Missing task ID. Pass it positionally (e.g. `task-123`) or with --task."
|
|
343
|
+
)
|
|
344
|
+
return task_id
|
|
345
|
+
|
|
346
|
+
|
|
295
347
|
def handle_validation_error(error: InputValidationError, output: Output) -> None:
|
|
296
348
|
"""Render input validation errors in the standard envelope."""
|
|
297
349
|
output.error(
|
|
@@ -35,6 +35,11 @@ def card() -> None:
|
|
|
35
35
|
@card.command("get")
|
|
36
36
|
@click.option("--url", "agent_url", help="Agent URL")
|
|
37
37
|
@click.option("--server", "-s", "server_name", help="Named server from servers.toml")
|
|
38
|
+
@click.option(
|
|
39
|
+
"--extended",
|
|
40
|
+
is_flag=True,
|
|
41
|
+
help="Fetch the extended card offered to authenticated clients",
|
|
42
|
+
)
|
|
38
43
|
@click.option(
|
|
39
44
|
"--bearer-env", "-b", help="Env var containing bearer token (overrides saved)"
|
|
40
45
|
)
|
|
@@ -44,6 +49,7 @@ def card() -> None:
|
|
|
44
49
|
def card_get(
|
|
45
50
|
agent_url: Optional[str],
|
|
46
51
|
server_name: Optional[str],
|
|
52
|
+
extended: bool,
|
|
47
53
|
bearer_env: Optional[str],
|
|
48
54
|
api_key_env: Optional[str],
|
|
49
55
|
) -> None:
|
|
@@ -54,6 +60,7 @@ def card_get(
|
|
|
54
60
|
$ handler card get --server my_agent
|
|
55
61
|
$ handler card get --url http://localhost:8000
|
|
56
62
|
$ handler card get --url http://localhost:8000 --bearer-env MY_TOKEN
|
|
63
|
+
$ handler card get --url http://localhost:8000 --extended --bearer-env MY_TOKEN
|
|
57
64
|
"""
|
|
58
65
|
output = Output()
|
|
59
66
|
|
|
@@ -74,7 +81,10 @@ def card_get(
|
|
|
74
81
|
try:
|
|
75
82
|
async with build_http_client(credentials=credentials) as http_client:
|
|
76
83
|
service = A2AService(http_client, resolved_url, credentials=credentials)
|
|
77
|
-
|
|
84
|
+
if extended:
|
|
85
|
+
card_data = await service.get_extended_card()
|
|
86
|
+
else:
|
|
87
|
+
card_data = await service.get_card()
|
|
78
88
|
log.info("Retrieved card for agent: %s", card_data.name)
|
|
79
89
|
|
|
80
90
|
_format_agent_card(card_data, output)
|
|
@@ -162,6 +172,7 @@ def card_validate(
|
|
|
162
172
|
source=resolved_url,
|
|
163
173
|
source_type=ValidationSource.URL,
|
|
164
174
|
agent_card=agent_card,
|
|
175
|
+
raw_data=service.raw_card,
|
|
165
176
|
)
|
|
166
177
|
|
|
167
178
|
_format_validation_result(result, output)
|
|
@@ -38,7 +38,7 @@ def mcp(transport: TransportType) -> None:
|
|
|
38
38
|
\b
|
|
39
39
|
Card Tools:
|
|
40
40
|
- validate_agent_card: Validate an agent card from URL or file
|
|
41
|
-
- get_agent_card: Retrieve an agent's
|
|
41
|
+
- get_agent_card: Retrieve an agent's public or extended card
|
|
42
42
|
|
|
43
43
|
\b
|
|
44
44
|
Message Tools:
|
|
@@ -47,9 +47,15 @@ def mcp(transport: TransportType) -> None:
|
|
|
47
47
|
\b
|
|
48
48
|
Task Tools:
|
|
49
49
|
- get_task: Get task status and details
|
|
50
|
+
- list_tasks: List an agent's tasks, following pagination to the end
|
|
50
51
|
- cancel_task: Cancel a running task
|
|
51
|
-
|
|
52
|
-
|
|
52
|
+
|
|
53
|
+
\b
|
|
54
|
+
Push Notification Tools:
|
|
55
|
+
- set_task_notification: Configure a push notification webhook
|
|
56
|
+
- get_task_notification: Get a task's push notification config
|
|
57
|
+
- list_task_notifications: List every push notification config for a task
|
|
58
|
+
- delete_task_notification: Delete a push notification config
|
|
53
59
|
|
|
54
60
|
\b
|
|
55
61
|
Session Tools:
|
|
@@ -57,10 +63,9 @@ def mcp(transport: TransportType) -> None:
|
|
|
57
63
|
- get_session_info: Get session for a specific agent
|
|
58
64
|
- clear_session_data: Clear saved session state
|
|
59
65
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
- clear_agent_credentials: Remove saved credentials
|
|
66
|
+
Every tool that talks to an agent accepts bearer_token, api_key, mTLS
|
|
67
|
+
paths, and custom_headers; credentials are only sent to the origin of
|
|
68
|
+
the given agent URL.
|
|
64
69
|
|
|
65
70
|
Example configuration for Claude Desktop (claude_desktop_config.json):
|
|
66
71
|
|